1+ // ==UserScript==
2+ // @name Bikemap.net Export GPX and KML routes
3+ // @description Download GPX, KML, TCX and geoJSON files for a route on bikemap.net
4+ // @namespace github.com/cvzi
5+ // @icon https://static.bikemap.net/favicons/apple-touch-icon.png
6+ // @match https://www.bikemap.net/*
7+ // @match https://web.bikemap.net/*
8+ // @connect www.bikemap.net
9+ // @version 1.3.0
10+ // @homepage https://github.com/cvzi/bikemapnet-userscript
11+ // @author cuzi
12+ // @license MIT
13+ // @grant GM.xmlHttpRequest
14+ // @grant GM.registerMenuCommand
15+ // @downloadURL https://update.greasyfork.org/scripts/445713/Bikemapnet%20Export%20GPX%20and%20KML%20routes.user.js
16+ // @updateURL https://update.greasyfork.org/scripts/445713/Bikemapnet%20Export%20GPX%20and%20KML%20routes.meta.js
17+ // ==/UserScript==
18+
19+ /*
20+ MIT License
21+
22+ Copyright (c) 2022, cuzi (https://openuserjs.org/users/cuzi)
23+
24+ Permission is hereby granted, free of charge, to any person obtaining a copy
25+ of this software and associated documentation files (the "Software"), to deal
26+ in the Software without restriction, including without limitation the rights
27+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
28+ copies of the Software, and to permit persons to whom the Software is
29+ furnished to do so, subject to the following conditions:
30+
31+ The above copyright notice and this permission notice shall be included in all
32+ copies or substantial portions of the Software.
33+
34+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40+ SOFTWARE.
41+ */
42+
43+ /* globals React, ReactDOM */
44+ ( function ( ) {
45+ 'use strict' ;
46+
47+ /* globals GM, sessionStorage, Blob */
48+
49+ if ( document . location . href . match ( / \/ r \/ ( \d + ) / ) ) {
50+ addDownloadButtons ( ) ;
51+ }
52+
53+ window . setInterval ( makeButtonsClickable , 3000 )
54+
55+ function makeButtonsClickable ( ) {
56+ if ( document . getElementById ( 'script_gpx_button' ) ) {
57+ return
58+ }
59+ const buttonGroup = document . querySelector ( '[class*=ButtonGroup_root]' )
60+
61+ const aGPX = document . createElement ( 'a' )
62+ aGPX . id = 'script_gpx_button'
63+ aGPX . href = '#'
64+ aGPX . textContent = 'GPX'
65+ aGPX . addEventListener ( 'click' , function ( ev ) {
66+ if ( ! ( 'ready' in this . dataset ) ) {
67+ ev . preventDefault ( )
68+ startDownload ( 'gpx' , this )
69+ }
70+ } )
71+ aGPX . className = buttonGroup . querySelector ( 'a' ) . className
72+ buttonGroup . appendChild ( aGPX )
73+
74+ const aKML = document . createElement ( 'a' )
75+ aKML . href = '#'
76+ aKML . textContent = 'KML'
77+ aKML . addEventListener ( 'click' , function ( ev ) {
78+ if ( ! ( 'ready' in this . dataset ) ) {
79+ ev . preventDefault ( )
80+ startDownload ( 'kml' , this )
81+ }
82+ } )
83+ aKML . className = buttonGroup . querySelector ( 'a' ) . className
84+ buttonGroup . appendChild ( aKML )
85+ }
86+
87+ function addDownloadButtons ( ) {
88+ GM . registerMenuCommand ( 'Download gpx' , ( ) => startDownload ( 'gpx' ) ) ;
89+ GM . registerMenuCommand ( 'Download kml' , ( ) => startDownload ( 'kml' ) ) ;
90+ }
91+
92+ function downloadUrl ( url , title , ext , button ) {
93+ if ( button ) {
94+ button . href = url
95+ button . style . color = 'green'
96+ button . dataset . ready = 1
97+ button . target = '_blank'
98+ window . setTimeout ( ( ) => button . click ( ) , 100 )
99+ } else {
100+ document . location . href = url
101+ }
102+ }
103+
104+
105+ function startDownload ( key , button ) {
106+ const routeId = parseInt ( document . location . href . match ( / \/ r \/ ( \d + ) / ) [ 1 ] ) ;
107+ cachedRequest ( {
108+ method : 'GET' ,
109+ url : `https://www.bikemap.net/api/v5/routes/${ routeId } /` ,
110+ headers : {
111+ 'x-requested-with' : 'XMLHttpRequest'
112+ } ,
113+ onload : function ( resp ) {
114+ const routeData = JSON . parse ( resp . responseText ) ;
115+ downloadUrl ( routeData [ key ] , routeData . title , key , button ) ;
116+ } ,
117+ onerror : function ( response ) {
118+ window . alert ( 'Error:' + response . status ) ;
119+ }
120+ } ) ;
121+
122+ }
123+
124+ function fileName ( title , ext ) {
125+ let name = title . replace ( / [: * ? <> / \\ , | \u0000 ] / g, '' ) ; // eslint-disable-line no-control-regex
126+
127+ name = name . trim ( ) . replace ( / ^ \. + / , '' ) . replace ( / \. + $ / , '' ) . trim ( ) ;
128+ return name + '.' + ext ;
129+ }
130+
131+ function cachedRequest ( obj ) {
132+ if ( 'data' in obj || obj . method !== 'GET' ) {
133+ return GM . xmlHttpRequest ( obj ) ;
134+ }
135+
136+ const cached = sessionStorage . getItem ( obj . url ) ;
137+
138+ if ( cached !== null ) {
139+ window . setTimeout ( function ( ) {
140+ const result = JSON . parse ( cached ) ;
141+ obj . onload ( result ) ;
142+ } , 1 ) ;
143+ } else {
144+ const orgOnload = obj . onload ;
145+
146+ obj . onload = function ( response ) {
147+ const newResponse = { } ;
148+
149+ for ( const key in response ) {
150+ newResponse [ key ] = response [ key ] ;
151+ }
152+
153+ newResponse . responseText = '' + response . responseText ;
154+ newResponse . cached = true ;
155+
156+ if ( ! ( 'time' in newResponse ) ) {
157+ newResponse . time = new Date ( ) . toJSON ( ) ;
158+ }
159+
160+ sessionStorage . setItem ( obj . url , JSON . stringify ( newResponse ) ) ;
161+ orgOnload . apply ( this , [ response ] ) ;
162+ } ;
163+
164+ GM . xmlHttpRequest ( obj ) ;
165+ }
166+ }
167+
168+ } ) ( ) ;
169+
0 commit comments