diff --git a/src/App.tsx b/src/App.tsx index df6d41af..6f045866 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,6 +41,7 @@ import Menu from '@/sidebar/menu.svg' import Cross from '@/sidebar/times-solid.svg' import PlainButton from '@/PlainButton' import useAreasLayer from '@/layers/UseAreasLayer' +import { Settings } from '@/stores/SettingsStore' export const POPUP_CONTAINER_ID = 'popup-container' export const SIDEBAR_CONTENT_ID = 'sidebar-content' @@ -99,7 +100,7 @@ export default function App() { // our different map layers useBackgroundLayer(map, mapOptions.selectedStyle) useMapBorderLayer(map, info.bbox) - useAreasLayer(map, query.customModelEnabled && query.customModelValid ? query.customModel?.areas! : null) + useAreasLayer(map, getCustomModelAreas(query)) useRoutingGraphLayer(map, mapOptions.routingGraphEnabled) useUrbanDensityLayer(map, mapOptions.urbanDensityEnabled) usePathsLayer(map, route.routingResult.paths, route.selectedPath) @@ -119,6 +120,7 @@ export default function App() { mapOptions={mapOptions} error={error} encodedValues={info.encoded_values} + settings={settings} /> ) : ( )} @@ -142,10 +145,12 @@ interface LayoutProps { mapOptions: MapOptionsStoreState error: ErrorStoreState encodedValues: object[] + settings: Settings } function LargeScreenLayout({ query, route, map, error, mapOptions, encodedValues }: LayoutProps) { const [showSidebar, setShowSidebar] = useState(true) + const [showCustomModelBox, setShowCustomModelBox] = useState(false) return ( <> {showSidebar ? ( @@ -157,15 +162,18 @@ function LargeScreenLayout({ query, route, map, error, mapOptions, encodedValues - setShowCustomModelBox(!showCustomModelBox)} + customModelBoxEnabled={query.customModelEnabled} /> + {showCustomModelBox && ( + + )}
{!error.isDismissed && }
- +
@@ -231,3 +245,12 @@ function SmallScreenLayout({ query, route, map, error, mapOptions }: LayoutProps ) } + +function getCustomModelAreas(queryStoreState: QueryStoreState): object | null { + if (!queryStoreState.customModelEnabled) return null + try { + return JSON.parse(queryStoreState.customModelStr)['areas'] + } catch { + return null + } +} diff --git a/src/NavBar.ts b/src/NavBar.ts index 05f11fdf..74c89525 100644 --- a/src/NavBar.ts +++ b/src/NavBar.ts @@ -36,8 +36,8 @@ export default class NavBar { result.searchParams.append('profile', queryStoreState.routingProfile.name) result.searchParams.append('layer', mapState.selectedStyle.name) - if (queryStoreState.customModelEnabled && queryStoreState.customModel && queryStoreState.customModelValid) - result.searchParams.append('custom_model', JSON.stringify(queryStoreState.customModel)) + if (queryStoreState.customModelEnabled) + result.searchParams.append('custom_model', queryStoreState.customModelStr.replace(/\s+/g, '')) return result } diff --git a/src/actions/Actions.ts b/src/actions/Actions.ts index 73e3367a..82c95fb7 100644 --- a/src/actions/Actions.ts +++ b/src/actions/Actions.ts @@ -77,7 +77,7 @@ export class InvalidatePoint implements Action { } } -export class SetCustomModelBoxEnabled implements Action { +export class SetCustomModelEnabled implements Action { readonly enabled: boolean constructor(enabled: boolean) { @@ -86,14 +86,12 @@ export class SetCustomModelBoxEnabled implements Action { } export class SetCustomModel implements Action { - readonly customModel: CustomModel | null - readonly valid: boolean - readonly issueRouteRequest - - constructor(customModel: CustomModel | null, valid: boolean, issueRouteRequest = false) { - this.customModel = customModel - this.valid = valid - this.issueRouteRequest = issueRouteRequest + readonly customModelStr: string + readonly issueRoutingRequest: boolean + + constructor(customModelStr: string, issueRoutingRequest: boolean) { + this.customModelStr = customModelStr + this.issueRoutingRequest = issueRoutingRequest } } diff --git a/src/sidebar/CustomModelBox.module.css b/src/sidebar/CustomModelBox.module.css index 2af51780..ce65461f 100644 --- a/src/sidebar/CustomModelBox.module.css +++ b/src/sidebar/CustomModelBox.module.css @@ -5,6 +5,20 @@ height: 18rem; } +.customModelOptionTable { + display: grid; + font-size: 14px; + grid-template-columns: 15% auto; + grid-template-rows: 1.3rem; + align-content: center; + align-items: center; + margin: 1.5rem 0.5rem 0.5rem 0; +} + +.customModelOptionTable svg { + scale: 0.7; +} + #element::-webkit-scrollbar { display: none; } diff --git a/src/sidebar/CustomModelBox.tsx b/src/sidebar/CustomModelBox.tsx index 78a4eb1f..c214d6e7 100644 --- a/src/sidebar/CustomModelBox.tsx +++ b/src/sidebar/CustomModelBox.tsx @@ -6,80 +6,40 @@ import styles from '@/sidebar/CustomModelBox.module.css' import { useCallback, useEffect, useRef, useState } from 'react' import { create } from 'custom-model-editor/src/index' import Dispatcher from '@/stores/Dispatcher' -import { DismissLastError, SetCustomModel } from '@/actions/Actions' -import { CustomModel } from '@/stores/QueryStore' +import { ClearRoute, DismissLastError, SetCustomModel, SetCustomModelEnabled } from '@/actions/Actions' import { tr } from '@/translation/Translation' import PlainButton from '@/PlainButton' +import { customModel2prettyString, customModelExamples } from '@/sidebar/CustomModelExamples' +import OnIcon from '@/sidebar/toggle_on.svg' +import OffIcon from '@/sidebar/toggle_off.svg' -const examples: { [key: string]: CustomModel } = { - default_example: { - distance_influence: 15, - priority: [{ if: 'road_environment == FERRY', multiply_by: '0.9' }], - speed: [], - areas: { - type: 'FeatureCollection', - features: [], - }, - }, - exclude_motorway: { - priority: [{ if: 'road_class == MOTORWAY', multiply_by: '0.0' }], - }, - limit_speed: { - speed: [ - { if: 'true', limit_to: '100' }, - { if: 'road_class == TERTIARY', limit_to: '80' }, - ], - }, - exclude_area: { - priority: [{ if: 'in_berlin_bbox', multiply_by: '0' }], - areas: { - type: 'FeatureCollection', - features: [ - { - type: 'Feature', - id: 'berlin_bbox', - properties: {}, - geometry: { - type: 'Polygon', - coordinates: [ - [ - [13.253, 52.608], - [13.228, 52.437], - [13.579, 52.447], - [13.563, 52.609], - [13.253, 52.608], - ], - ], - }, - }, - ], - }, - }, - cargo_bike: { - speed: [{ if: 'road_class == TRACK', limit_to: '2' }], - priority: [{ if: 'max_width < 1.5 || road_class == STEPS', multiply_by: '0' }], - }, - combined: { - distance_influence: 100, - speed: [{ if: 'road_class == TRACK || road_environment == FERRY || surface == DIRT', limit_to: '10' }], - priority: [ - { if: 'road_environment == TUNNEL || toll == ALL', multiply_by: '0.5' }, - { if: 'max_weight < 3 || max_height < 2.5', multiply_by: '0.0' }, - ], - }, +function convertEncodedValuesForEditor(encodedValues: object[]): any { + // todo: maybe do this 'conversion' in Api.ts already and use types from there on + const categories: any = {} + Object.keys(encodedValues).forEach((k: any) => { + const v: any = encodedValues[k] + if (v.length === 2 && v[0] === 'true' && v[1] === 'false') { + categories[k] = { type: 'boolean' } + } else if (v.length === 2 && v[0] === '>number' && v[1] === '(null) useEffect(() => { - // we start with empty categories. they will be set later using info - const instance = create({}, (element: Node) => divElement.current?.appendChild(element)) + // we start with the encoded values we already have, but they might be empty still + const instance = create(convertEncodedValuesForEditor(encodedValues), (element: Node) => + divElement.current?.appendChild(element) + ) setEditor(instance) instance.cm.setSize('100%', '100%') - if (initialCustomModelStr != null) { - // prettify the custom model if it can be parsed or leave it as is otherwise - try { - initialCustomModelStr = customModel2prettyString(JSON.parse(initialCustomModelStr)) - } catch (e) {} - } - instance.value = - initialCustomModelStr == null - ? customModel2prettyString(examples['default_example']) - : initialCustomModelStr - - if (enabled) - // When we got a custom model from the url parameters we send the request right away - dispatchCustomModel(instance.value, true, true) - - instance.validListener = (valid: boolean) => { - // We update the app states' custom model, but we are not requesting a routing query every time the model - // becomes valid. Updating the model is still important, because the routing request might be triggered by - // moving markers etc. - dispatchCustomModel(instance.value, valid, false) - setIsValid(valid) - } + instance.cm.on('change', () => Dispatcher.dispatch(new SetCustomModel(instance.value, false))) + instance.validListener = (valid: boolean) => setIsValid(valid) }, []) - // without this the editor is blank after opening the box and before clicking it or resizing the window? - // but having the focus in the box after opening it is nice anyway - useEffect(() => { - if (enabled) editor?.cm.focus() - }, [enabled]) - useEffect(() => { if (!editor) return - - // todo: maybe do this 'conversion' in Api.ts already and use types from there on - const categories: any = {} - Object.keys(encodedValues).forEach((k: any) => { - const v: any = encodedValues[k] - if (v.length === 2 && v[0] === 'true' && v[1] === 'false') { - categories[k] = { type: 'boolean' } - } else if (v.length === 2 && v[0] === '>number' && v[1] === ') => { if (event.ctrlKey && event.key === 'Enter') { // Using this keyboard shortcut we can skip the custom model validation and directly request a routing // query. - const isValid = true - dispatchCustomModel(editor.value, isValid, true) + Dispatcher.dispatch(new SetCustomModel(editor.value, true)) } }, [editor, isValid] @@ -155,70 +80,66 @@ export default function CustomModelBox({ return ( <> - {/*we use 'display: none' instead of conditional rendering to preserve the custom model box's state when it is closed*/} -
- {enabled && ( -
- +
+ { + if (customModelEnabled) Dispatcher.dispatch(new DismissLastError()) + Dispatcher.dispatch(new ClearRoute()) + Dispatcher.dispatch(new SetCustomModelEnabled(!customModelEnabled)) + }} + > + {customModelEnabled ? : } + +
{tr('custom_model_enabled')}
+
+
+
+ - - {tr('help_custom_model')} - -
+ {tr('help_custom_model')} + +
+ { + if (!customModelEnabled) Dispatcher.dispatch(new SetCustomModelEnabled(true)) + Dispatcher.dispatch(new SetCustomModel(editor.value, true)) + }} > - dispatchCustomModel(editor.value, true, true)} - > - {tr('apply_custom_model')} - - {queryOngoing &&
} -
+ {tr('apply_custom_model')} + + {queryOngoing &&
}
- )} +
) } - -function dispatchCustomModel(customModelString: string, isValid: boolean, withRouteRequest: boolean) { - try { - const parsedValue = JSON.parse(customModelString) - Dispatcher.dispatch(new SetCustomModel(parsedValue, isValid, withRouteRequest)) - } catch (e) { - Dispatcher.dispatch(new SetCustomModel(null, false, withRouteRequest)) - } -} - -function customModel2prettyString(customModel: CustomModel) { - return JSON.stringify(customModel, null, 2) -} diff --git a/src/sidebar/CustomModelExamples.ts b/src/sidebar/CustomModelExamples.ts new file mode 100644 index 00000000..8e4511aa --- /dev/null +++ b/src/sidebar/CustomModelExamples.ts @@ -0,0 +1,63 @@ +import { CustomModel } from '@/stores/QueryStore' + +export const customModelExamples: { [key: string]: CustomModel } = { + default_example: { + distance_influence: 15, + priority: [{ if: 'road_environment == FERRY', multiply_by: '0.9' }], + speed: [], + areas: { + type: 'FeatureCollection', + features: [], + }, + }, + exclude_motorway: { + priority: [{ if: 'road_class == MOTORWAY', multiply_by: '0.0' }], + }, + limit_speed: { + speed: [ + { if: 'true', limit_to: '100' }, + { if: 'road_class == TERTIARY', limit_to: '80' }, + ], + }, + exclude_area: { + priority: [{ if: 'in_berlin_bbox', multiply_by: '0' }], + areas: { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + id: 'berlin_bbox', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [13.253, 52.608], + [13.228, 52.437], + [13.579, 52.447], + [13.563, 52.609], + [13.253, 52.608], + ], + ], + }, + }, + ], + }, + }, + cargo_bike: { + speed: [{ if: 'road_class == TRACK', limit_to: '2' }], + priority: [{ if: 'max_width < 1.5 || road_class == STEPS', multiply_by: '0' }], + }, + combined: { + distance_influence: 100, + speed: [{ if: 'road_class == TRACK || road_environment == FERRY || surface == DIRT', limit_to: '10' }], + priority: [ + { if: 'road_environment == TUNNEL || toll == ALL', multiply_by: '0.5' }, + { if: 'max_weight < 3 || max_height < 2.5', multiply_by: '0.0' }, + ], + }, +} + +export function customModel2prettyString(customModel: CustomModel) { + return JSON.stringify(customModel, null, 2) +} diff --git a/src/sidebar/MobileSidebar.tsx b/src/sidebar/MobileSidebar.tsx index 1c683f19..c7807619 100644 --- a/src/sidebar/MobileSidebar.tsx +++ b/src/sidebar/MobileSidebar.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react' -import { QueryPoint, QueryPointType, QueryStoreState } from '@/stores/QueryStore' +import { QueryPoint, QueryPointType, QueryStoreState, RequestState } from '@/stores/QueryStore' import { RouteStoreState } from '@/stores/RouteStore' import { ErrorStoreState } from '@/stores/ErrorStore' import styles from './MobileSidebar.module.css' @@ -10,14 +10,19 @@ import { MarkerComponent } from '@/map/Marker' import RoutingProfiles from '@/sidebar/search/routingProfiles/RoutingProfiles' import OpenInputsIcon from './unfold.svg' import CloseInputsIcon from './unfold_less.svg' +import CustomModelBox from '@/sidebar/CustomModelBox' +import { Settings } from '@/stores/SettingsStore' type MobileSidebarProps = { query: QueryStoreState route: RouteStoreState error: ErrorStoreState + encodedValues: object[] + settings: Settings } -export default function ({ query, route, error }: MobileSidebarProps) { +export default function ({ query, route, error, encodedValues, settings }: MobileSidebarProps) { + const [showCustomModelBox, setShowCustomModelBox] = useState(false) // the following three elements control, whether the small search view is displayed const isShortScreen = useMediaQuery({ query: '(max-height: 55rem)' }) const [isSmallSearchView, setIsSmallSearchView] = useState(isShortScreen && hasResult(route)) @@ -55,9 +60,18 @@ export default function ({ query, route, error }: MobileSidebarProps) { setShowCustomModelBox(!showCustomModelBox)} + customModelBoxEnabled={query.customModelEnabled} /> + {showCustomModelBox && ( + + )}
)} diff --git a/src/sidebar/SettingsBox.module.css b/src/sidebar/SettingsBox.module.css new file mode 100644 index 00000000..660a2e4d --- /dev/null +++ b/src/sidebar/SettingsBox.module.css @@ -0,0 +1,50 @@ +.parent { + margin-top: 15px; + padding: 15px 5px 10px 5px; + border-top: 1px lightgray solid; + border-bottom: 1px lightgray solid; +} + +.settingsTable { + display: grid; + grid-template-columns: 50px auto; + grid-template-rows: 1.3rem; + align-content: flex-start; + align-items: center; + margin: 1rem 0.5rem 1.5rem 0; +} + +.settingsTable svg { + scale: 0.7; + margin-left: calc(-8px); /* scaling svg creates white space around it */ +} + +.settingsTable button { +} + +.infoLine { + padding: 10px 2px 0 0; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.infoLine a { + color: gray; + text-decoration: none; +} + +.infoLine a:hover { + color: black; +} + +.title { + font-weight: bold; +} + +.title, +.infoLine, +.settingsTable { + font-size: 14px; + color: gray; +} diff --git a/src/sidebar/SettingsBox.tsx b/src/sidebar/SettingsBox.tsx new file mode 100644 index 00000000..d1a08f06 --- /dev/null +++ b/src/sidebar/SettingsBox.tsx @@ -0,0 +1,36 @@ +import { ToggleDistanceUnits } from '@/actions/Actions' +import Dispatcher from '@/stores/Dispatcher' +import styles from '@/sidebar/SettingsBox.module.css' +import { tr } from '@/translation/Translation' +import PlainButton from '@/PlainButton' +import OnIcon from '@/sidebar/toggle_on.svg' +import OffIcon from '@/sidebar/toggle_off.svg' +import { useContext } from 'react' +import { ShowDistanceInMilesContext } from '@/ShowDistanceInMilesContext' + +export default function SettingsBox() { + const showDistanceInMiles = useContext(ShowDistanceInMilesContext) + return ( +
+
{tr('settings')}
+
+ Dispatcher.dispatch(new ToggleDistanceUnits())} + > + {showDistanceInMiles ? : } + +
+ {tr('distance_unit', [tr(showDistanceInMiles ? 'mi' : 'km')])} +
+
+ +
+ ) +} diff --git a/src/sidebar/settings.svg b/src/sidebar/open_custom_model.svg similarity index 99% rename from src/sidebar/settings.svg rename to src/sidebar/open_custom_model.svg index 8e649588..8486b0cf 100644 --- a/src/sidebar/settings.svg +++ b/src/sidebar/open_custom_model.svg @@ -40,6 +40,7 @@ id="layer1" inkscape:highlight-color="#729fcf"> @@ -66,26 +57,11 @@ export default function Search({ points }: { points: QueryPoint[] }) {
{tr('add_to_route')}
- Dispatcher.dispatch(new ToggleDistanceUnits())} - > - {showDistanceInMiles ? 'mi' : 'km'} - - setShowInfo(!showInfo)}> - + setShowSettings(!showSettings)}> + {showSettings ? tr('settings_close') : tr('settings')}
- {showInfo && ( - - )} + {showSettings && }
) } diff --git a/src/sidebar/search/routingProfiles/RoutingProfiles.module.css b/src/sidebar/search/routingProfiles/RoutingProfiles.module.css index 1da6b9ee..3b583108 100644 --- a/src/sidebar/search/routingProfiles/RoutingProfiles.module.css +++ b/src/sidebar/search/routingProfiles/RoutingProfiles.module.css @@ -28,50 +28,58 @@ transform: scale(1.1); } -.enabledSettings, -.settings { +.asIndicator { + position: absolute; + left: -21px; + top: -40px; + z-index: 1; + scale: 0.1; +} + +.enabledCMBox, +.cmBox { border-radius: 50%; } -.enabledSettings svg path, -.settings svg path { +.enabledCMBox svg path, +.cmBox svg path { fill: #a8a8a8; stroke: #a8a8a8; } -.enabledSettings:active svg path, -.settings:active svg path { +.enabledCMBox:active svg path, +.cmBox:active svg path { fill: lightgray; stroke: lightgray; } -.settings:hover svg path, -.enabledSettings:hover svg path { +.cmBox:hover svg path, +.enabledCMBox:hover svg path { fill: #000000; stroke: #000000; } -.settings:active svg path, -.enabledSettings:active svg path { +.cmBox:active svg path, +.enabledCMBox:active svg path { fill: lightgray; stroke: lightgray; } -.enabledSettings svg, -.settings svg { +.enabledCMBox svg, +.cmBox svg { margin: 0; padding: 8px; width: 18px; height: 18px; } -.settings svg { +.cmBox svg { transform: rotate(0); animation-name: reverse_rotate; animation-duration: 0.2s; } -.enabledSettings svg { +.enabledCMBox svg { transform: rotate(180deg); animation-name: setting_rotate; animation-duration: 0.2s; diff --git a/src/sidebar/search/routingProfiles/RoutingProfiles.tsx b/src/sidebar/search/routingProfiles/RoutingProfiles.tsx index aedcebff..afce4067 100644 --- a/src/sidebar/search/routingProfiles/RoutingProfiles.tsx +++ b/src/sidebar/search/routingProfiles/RoutingProfiles.tsx @@ -1,7 +1,7 @@ import React from 'react' import styles from './RoutingProfiles.module.css' import Dispatcher from '@/stores/Dispatcher' -import { ClearRoute, DismissLastError, SetCustomModelBoxEnabled, SetVehicleProfile } from '@/actions/Actions' +import { SetVehicleProfile } from '@/actions/Actions' import { RoutingProfile } from '@/api/graphhopper' import PlainButton from '@/PlainButton' import BicycleIcon from './bike.svg' @@ -16,34 +16,30 @@ import SmallTruckIcon from './small_truck.svg' import TruckIcon from './truck.svg' import WheelchairIcon from './wheelchair.svg' import { tr } from '@/translation/Translation' -import SettingsSVG from '@/sidebar/settings.svg' +import CustomModelBoxSVG from '@/sidebar/open_custom_model.svg' export default function ({ routingProfiles, selectedProfile, - customModelAllowed, - customModelEnabled, + showCustomModelBox, + toggleCustomModelBox, + customModelBoxEnabled, }: { routingProfiles: RoutingProfile[] selectedProfile: RoutingProfile - customModelAllowed: boolean - customModelEnabled: boolean + showCustomModelBox: boolean + toggleCustomModelBox: () => void + customModelBoxEnabled: boolean }) { return (
- {customModelAllowed && ( - { - if (customModelEnabled) Dispatcher.dispatch(new DismissLastError()) - Dispatcher.dispatch(new ClearRoute()) - Dispatcher.dispatch(new SetCustomModelBoxEnabled(!customModelEnabled)) - }} - > - - - )} + + +
    {routingProfiles.map(profile => { const className = @@ -57,6 +53,9 @@ export default function ({ onClick={() => Dispatcher.dispatch(new SetVehicleProfile(profile))} className={className} > + {customModelBoxEnabled && profile.name === selectedProfile.name && ( + + )} {getIcon(profile)} diff --git a/src/sidebar/toggle_off.svg b/src/sidebar/toggle_off.svg new file mode 100644 index 00000000..a83c2d72 --- /dev/null +++ b/src/sidebar/toggle_off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/sidebar/toggle_on.svg b/src/sidebar/toggle_on.svg new file mode 100644 index 00000000..17dc978c --- /dev/null +++ b/src/sidebar/toggle_on.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/stores/QueryStore.ts b/src/stores/QueryStore.ts index 988c354d..64cb75a0 100644 --- a/src/stores/QueryStore.ts +++ b/src/stores/QueryStore.ts @@ -12,8 +12,8 @@ import { RemovePoint, RouteRequestFailed, RouteRequestSuccess, + SetCustomModelEnabled, SetCustomModel, - SetCustomModelBoxEnabled, SetPoint, SetQueryPoints, SetVehicleProfile, @@ -21,6 +21,7 @@ import { import { RoutingArgs, RoutingProfile } from '@/api/graphhopper' import { calcDist } from '@/distUtils' import config from 'config' +import { customModel2prettyString, customModelExamples } from '@/sidebar/CustomModelExamples' export interface Coordinate { lat: number @@ -35,12 +36,9 @@ export interface QueryStoreState { readonly maxAlternativeRoutes: number readonly routingProfile: RoutingProfile readonly customModelEnabled: boolean - readonly customModelValid: boolean - readonly customModel: CustomModel | null + readonly customModelStr: string // todo: probably this should go somewhere else, see: https://github.com/graphhopper/graphhopper-maps/pull/193 readonly zoom: boolean - // todo: ... and this also - readonly initialCustomModelStr: string | null } export interface QueryPoint { @@ -89,6 +87,14 @@ export default class QueryStore extends Store { } private static getInitialState(initialCustomModelStr: string | null): QueryStoreState { + const customModelEnabledInitially = initialCustomModelStr != null + if (!initialCustomModelStr) + initialCustomModelStr = customModel2prettyString(customModelExamples['default_example']) + // prettify the custom model if it can be parsed or leave it as is otherwise + try { + initialCustomModelStr = customModel2prettyString(JSON.parse(initialCustomModelStr)) + } catch (e) {} + return { profiles: [], queryPoints: [ @@ -103,11 +109,9 @@ export default class QueryStore extends Store { routingProfile: { name: '', }, - customModelEnabled: initialCustomModelStr != null, - customModelValid: false, - customModel: null, + customModelEnabled: customModelEnabledInitially, + customModelStr: initialCustomModelStr, zoom: true, - initialCustomModelStr: initialCustomModelStr, } } @@ -260,17 +264,14 @@ export default class QueryStore extends Store { return this.routeIfReady(newState) } else if (action instanceof SetCustomModel) { - const newState: QueryStoreState = { + const newState = { ...state, - customModel: action.customModel, - customModelValid: action.valid, + customModelStr: action.customModelStr, } - - if (action.issueRouteRequest) return this.routeIfReady(newState) - else return newState + return action.issueRoutingRequest ? this.routeIfReady(newState) : newState } else if (action instanceof RouteRequestSuccess || action instanceof RouteRequestFailed) { return QueryStore.handleFinishedRequest(state, action) - } else if (action instanceof SetCustomModelBoxEnabled) { + } else if (action instanceof SetCustomModelEnabled) { const newState: QueryStoreState = { ...state, customModelEnabled: action.enabled, @@ -363,9 +364,13 @@ export default class QueryStore extends Store { } private static isReadyToRoute(state: QueryStoreState) { - // deliberately chose this style of if statements, to make this readable. - if (state.customModelEnabled && !state.customModel) return false - if (state.customModelEnabled && state.customModel && !state.customModelValid) return false + if (state.customModelEnabled) + try { + JSON.parse(state.customModelStr) + } catch { + return false + } + // Janek deliberately chose this style of if statements, to make this readable. if (state.queryPoints.length <= 1) return false if (!state.queryPoints.every(point => point.isInitialized)) return false if (!state.routingProfile.name) return false @@ -430,11 +435,17 @@ export default class QueryStore extends Store { number ][] + let customModel = null + if (state.customModelEnabled) + try { + customModel = JSON.parse(state.customModelStr) + } catch {} + return { points: coordinates, profile: state.routingProfile.name, maxAlternativeRoutes: state.maxAlternativeRoutes, - customModel: state.customModelEnabled ? state.customModel : null, + customModel: customModel, zoom: state.zoom, } } diff --git a/src/translation/tr.json b/src/translation/tr.json index 69245f27..83875dee 100644 --- a/src/translation/tr.json +++ b/src/translation/tr.json @@ -26,6 +26,9 @@ "open_custom_model_box":"Open custom model box", "help_custom_model":"Help", "apply_custom_model":"Apply", +"custom_model_enabled":"Custom Model Active", +"settings":"Settings", +"settings_close":"Close", "exclude_motorway_example":"Exclude Motorway", "limit_speed_example":"Limit Speed", "cargo_bike_example":"Cargo Bike", @@ -102,6 +105,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -178,6 +184,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -254,6 +263,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -330,6 +342,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -406,6 +421,9 @@ "open_custom_model_box":"", "help_custom_model":"সাহায্য", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -482,6 +500,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -531,25 +552,25 @@ "waiting_for_gps":"" }, "cs_CZ":{ -"total_ascend":"Celkové stoupání %1$s", -"total_descend":"Celkové klesání %1$s", -"way_contains_ford":"na cestě je brod", -"way_contains_ferry":"", -"way_contains_private":"", -"way_contains_toll":"", -"way_crosses_border":"", -"way_contains":"", -"tracks":"", -"steps":"", -"footways":"", -"steep_sections":"", +"total_ascend":"celkové stoupání %1$s", +"total_descend":"celkové klesání %1$s", +"way_contains_ford":"Trasa obsahuje brody", +"way_contains_ferry":"Trasa obsahuje přívozy", +"way_contains_private":"Trasa obsahuje soukromé cesty", +"way_contains_toll":"Trasa obsahuje zpoplatněné úseky", +"way_crosses_border":"Trasa obsahuje překročení hranic", +"way_contains":"Trasa obsahuje %1$s", +"tracks":"nezpevněné cesty", +"steps":"schody", +"footways":"stezky pro pěší", +"steep_sections":"příkré úseky", "start_label":"Start", "intermediate_label":"Zastávka", "end_label":"Cíl", "set_start":"Nastavit jako start", "set_intermediate":"Nastavit jako zastávku", "set_end":"Nastavit jako cíl", -"center_map":"Vycentrovat sem mapu", +"center_map":"Vycentrovat mapu sem", "show_coords":"Zobrazit souřadnice", "query_osm":"", "route":"Trasa", @@ -558,12 +579,15 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", -"exclude_motorway_example":"", -"limit_speed_example":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", +"exclude_motorway_example":"Vynechat dálnice", +"limit_speed_example":"Rychlostní omezení", "cargo_bike_example":"", -"exclude_area_example":"", +"exclude_area_example":"Vynechat oblast", "combined_example":"", -"examples_custom_model":"", +"examples_custom_model":"Příklady", "marker":"Značka", "gh_offline_info":"API GraphHopper je offline?", "refresh_button":"Obnovit stránku", @@ -571,12 +595,12 @@ "zoom_in":"Přiblížit", "zoom_out":"Oddálit", "drag_to_reorder":"Přetažením změníte pořadí", -"route_timed_out":"", -"route_request_failed":"", -"current_location":"", +"route_timed_out":"Časový limit výpočtu trasy překročen", +"route_request_failed":"Trasa nemohla být vypočítána", +"current_location":"Aktuální pozice", "searching_location":"", "searching_location_failed":"", -"via_hint":"Přes ", +"via_hint":"Přes", "from_hint":"Z", "gpx_export_button":"Export do GPX", "gpx_button":"", @@ -586,9 +610,9 @@ "route_info":"%1$s bude trvat %2$s", "search_button":"Vyhledat", "more_button":"více", -"pt_route_info":"dorazí v %1$s, %2$s přestup(y) (%3$s) ", -"pt_route_info_walking":"dorazí v %1$s (%2$s)", -"locations_not_found":"Navigování není dostupné. Pozice nenalezena v této oblasti.", +"pt_route_info":"dorazí v %1$s, %2$s přestup(y) (%3$s)", +"pt_route_info_walking":"dorazí v %1$s pěšky (%2$s)", +"locations_not_found":"", "search_with_nominatim":"", "powered_by":"", "bike":"Kolo", @@ -600,11 +624,11 @@ "small_truck":"Dodávka", "bus":"Autobus", "truck":"Nákladní automobil", -"staticlink":"neměnný odkaz", +"staticlink":"statický odkaz", "motorcycle":"Motocykl", -"back_to_map":"", -"distance_unit":"", -"waiting_for_gps":"" +"back_to_map":"Zpět", +"distance_unit":"Vzdálenosti jsou uváděny v %1$s", +"waiting_for_gps":"Čekání na signál GPS…" }, "da_DK":{ "total_ascend":"%1$s samlet stigning", @@ -634,6 +658,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -710,6 +737,9 @@ "open_custom_model_box":"Öffne custom model Kasten", "help_custom_model":"Hilfe", "apply_custom_model":"Anwenden", +"custom_model_enabled":"Custom Model aktiv", +"settings":"Optionen", +"settings_close":"Schließen", "exclude_motorway_example":"Autobahn vermeiden", "limit_speed_example":"Max. Geschwindigkeit", "cargo_bike_example":"Lastenfahrrad", @@ -731,7 +761,7 @@ "via_hint":"Über", "from_hint":"Von", "gpx_export_button":"GPX Export", -"gpx_button":"", +"gpx_button":"GPX", "hide_button":"Weniger", "details_button":"Details", "to_hint":"Nach", @@ -786,6 +816,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -862,6 +895,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -938,6 +974,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -1014,6 +1053,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -1090,6 +1132,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -1166,6 +1211,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -1225,8 +1273,8 @@ "way_contains":"L'itinéraire inclut %1$s", "tracks":"chemins de terre non pavés", "steps":"pas", -"footways":"", -"steep_sections":"", +"footways":"chemin pédestre", +"steep_sections":"section raide", "start_label":"Départ", "intermediate_label":"Point intermédiaire", "end_label":"Arrivée", @@ -1242,9 +1290,12 @@ "open_custom_model_box":"Ouvrir la boîte du modèle personnalisé", "help_custom_model":"Aide", "apply_custom_model":"Appliquer", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"Eviter les Autoroutes", "limit_speed_example":"Vitesse Limite", -"cargo_bike_example":"", +"cargo_bike_example":"Vélo cargo", "exclude_area_example":"Exclure Zone", "combined_example":"Exemple Combiné", "examples_custom_model":"Examples", @@ -1318,6 +1369,9 @@ "open_custom_model_box":"Ouvrir la boîte du modèle personnalisé", "help_custom_model":"Aide", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -1394,6 +1448,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -1470,6 +1527,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -1546,6 +1606,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -1622,6 +1685,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -1698,6 +1764,9 @@ "open_custom_model_box":"Egyedi modelldoboz megnyitása", "help_custom_model":"Súgó", "apply_custom_model":"Alkalmazás", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"Autópálya nélkül", "limit_speed_example":"Sebességkorlátozás", "cargo_bike_example":"", @@ -1774,6 +1843,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -1850,6 +1922,9 @@ "open_custom_model_box":"", "help_custom_model":"Aiuto", "apply_custom_model":"Applica", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"Escludi autostrada", "limit_speed_example":"Limita velocità", "cargo_bike_example":"", @@ -1926,6 +2001,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -1974,6 +2052,85 @@ "distance_unit":"", "waiting_for_gps":"" }, +"kk":{ +"total_ascend":"%1$s көтерілу", +"total_descend":"%1$s төмен түсу", +"way_contains_ford":"Жолда өткел бар", +"way_contains_ferry":" паромға отырыңыз", +"way_contains_private":"жекеменшік жол", +"way_contains_toll":"жол ақылы", +"way_crosses_border":"жол ел шекарасын кесіп жатыр", +"way_contains":"жолда %1$s бар", +"tracks":"асфальтталмаған қара жолдар", +"steps":"қадамдар", +"footways":"жаяу жүргіншілер жолы", +"steep_sections":"күрделі аялдамалар", +"start_label":"бастапқы нүкте", +"intermediate_label":"аралық нүкте", +"end_label":"соңғы нүкте", +"set_start":"бастапқы нүктені таңдау", +"set_intermediate":"аралық нүктені қосу", +"set_end":"соңғы нүктені таңдау", +"center_map":"", +"show_coords":"координаталарды көрсету", +"query_osm":"", +"route":"маршрут", +"add_to_route":"", +"delete_from_route":"маршруттан жою", +"open_custom_model_box":"Арнайы модель қорабын ашыңыз", +"help_custom_model":"көмек", +"apply_custom_model":"қолдану", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", +"exclude_motorway_example":"Автомогистральді алып тастау", +"limit_speed_example":"шектеулі жылдамдық", +"cargo_bike_example":"жүк велосипеді", +"exclude_area_example":"Аймақты алып тастау", +"combined_example":"", +"examples_custom_model":"мысалдар", +"marker":"белгі", +"gh_offline_info":"", +"refresh_button":"бетті жаңартыңыз", +"server_status":"жағдайы", +"zoom_in":"үлкейту", +"zoom_out":"кішірейту", +"drag_to_reorder":"Ретін өзгерту үшін жылжытыңыз", +"route_timed_out":"Маршрутты есептеу уақыты таусылды", +"route_request_failed":"маршрут сұранысы орындалмады", +"current_location":"қазіргі орныңыз", +"searching_location":"орналасқан жерді іздеу", +"searching_location_failed":"Қазіргі орныңызды іздеу сәтсіз аяқталды", +"via_hint":"арқылы", +"from_hint":"Бастауы", +"gpx_export_button":"GPX Экспорты", +"gpx_button":"GPX", +"hide_button":"жасыру", +"details_button":"толығырақ", +"to_hint":"дейін", +"route_info":"%1$s-ге дейін шамамен %2$s уақыт кетеді", +"search_button":"іздеу", +"more_button":"тағы", +"pt_route_info":"%2$s-ден %1$s-ге пересадкамен бару (%3$s)", +"pt_route_info_walking":"%1$s-ге дейін тек жаяу жүру (%2$s)", +"locations_not_found":"Маршрут құру мүмкін болмады. Орын анықталмады.", +"search_with_nominatim":"Nominatim арқылы іздеу", +"powered_by":"", +"bike":"велосипед", +"racingbike":"", +"mtb":"Таулы велосипед", +"car":"көлік", +"foot":"жаяу жүргінші", +"hike":"", +"small_truck":"Шағын жүк көлігі", +"bus":"автобус", +"truck":"үлкен жүк көлігі", +"staticlink":"", +"motorcycle":"мотоцикл", +"back_to_map":"Қайту", +"distance_unit":"", +"waiting_for_gps":"GPS сигналын күтіңіз..." +}, "ko":{ "total_ascend":"오르막길 총 %1$s", "total_descend":"내리막길 총 %1$s", @@ -2002,6 +2159,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -2078,6 +2238,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -2154,6 +2317,9 @@ "open_custom_model_box":"Åpne tilpasser modellboks", "help_custom_model":"Hjelp", "apply_custom_model":"Bekreft", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"Unngå motorvei", "limit_speed_example":"Begrens hastighet", "cargo_bike_example":"", @@ -2230,6 +2396,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -2306,6 +2475,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -2382,6 +2554,9 @@ "open_custom_model_box":"Otwórz okienko modelu niestandardowego", "help_custom_model":"Pomoc", "apply_custom_model":"Zastosuj", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"Pomijaj autostrady", "limit_speed_example":"Ogranicz prędkość", "cargo_bike_example":"Rower towarowy", @@ -2458,6 +2633,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -2534,6 +2712,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -2610,6 +2791,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -2686,6 +2870,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -2762,6 +2949,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -2838,6 +3028,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -2914,6 +3107,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -2990,6 +3186,9 @@ "open_custom_model_box":"", "help_custom_model":"Hjälp", "apply_custom_model":"Applicera", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -3066,6 +3265,9 @@ "open_custom_model_box":"", "help_custom_model":"Yardım", "apply_custom_model":"Uygula", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"Hız limiti", "cargo_bike_example":"", @@ -3142,6 +3344,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -3218,6 +3423,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -3294,6 +3502,9 @@ "open_custom_model_box":"打开自定义模型选项", "help_custom_model":"帮助", "apply_custom_model":"应用", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"排除高速公路", "limit_speed_example":"限速", "cargo_bike_example":"", @@ -3370,6 +3581,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", @@ -3446,6 +3660,9 @@ "open_custom_model_box":"", "help_custom_model":"", "apply_custom_model":"", +"custom_model_enabled":"", +"settings":"", +"settings_close":"", "exclude_motorway_example":"", "limit_speed_example":"", "cargo_bike_example":"", diff --git a/update-translations.py b/update-translations.py index 4f946b68..b74f8d4a 100644 --- a/update-translations.py +++ b/update-translations.py @@ -3,7 +3,7 @@ import requests destination = "./src/translation/tr.json" -translations = "en_US SKIP SKIP ar ast az bg bn_BN ca cs_CZ da_DK de_DE el eo es fa fil fi fr_FR fr_CH gl he hr_HR hsb hu_HU in_ID it ja ko lt_LT nb_NO ne nl pl_PL pt_BR pt_PT ro ru sk sl_SI sr_RS sv_SE tr uk vi_VN zh_CN zh_HK zh_TW" +translations = "en_US SKIP SKIP ar ast az bg bn_BN ca cs_CZ da_DK de_DE el eo es fa fil fi fr_FR fr_CH gl he hr_HR hsb hu_HU in_ID it ja kk ko lt_LT nb_NO ne nl pl_PL pt_BR pt_PT ro ru sk sl_SI sr_RS sv_SE tr uk vi_VN zh_CN zh_HK zh_TW" r = requests.get("https://docs.google.com/spreadsheets/d/10HKSFmxGVEIO92loVQetVmjXT0qpf3EA2jxuQSSYTdU/export?format=tsv&id=10HKSFmxGVEIO92loVQetVmjXT0qpf3EA2jxuQSSYTdU&gid=0") r.encoding = 'utf-8' # useful if encoding is not sent (or not sent properly) by the server