diff --git a/app/helpers/gtt_map_helper.rb b/app/helpers/gtt_map_helper.rb index caca1044..fb216248 100644 --- a/app/helpers/gtt_map_helper.rb +++ b/app/helpers/gtt_map_helper.rb @@ -32,7 +32,6 @@ def map_tag(map: nil, layers: map&.layers, data[:popup] = popup if popup data[:upload] = upload data[:collapsed] = collapsed if collapsed - data[:geocoding] = true if Setting.plugin_redmine_gtt['enable_geocoding_on_map'] == 'true' uid = "ol-" + rand(36**8).to_s(36) diff --git a/app/views/settings/gtt/_geocoder.html.erb b/app/views/settings/gtt/_geocoder.html.erb index c0c6c6db..49e053d4 100644 --- a/app/views/settings/gtt/_geocoder.html.erb +++ b/app/views/settings/gtt/_geocoder.html.erb @@ -6,6 +6,19 @@ <%= check_box_tag 'settings[enable_geocoding_on_map]', true, @settings[:enable_geocoding_on_map] %>

+

+ <%= content_tag(:label, l(:geocoder_provider)) %> + <%= select_tag 'settings[default_geocoder_provider]', + options_for_select([ + ['Google', 'google'], + ['Nominatim (OSM)', 'nominatim'], + ['Photon', 'photon'], + ['Custom', 'custom', {disabled: true}] + ], @settings['default_geocoder_provider']), + include_blank: true %> + <%= link_to t('geocoder_load_example'), '#', id: 'geocoder_load_example', class: 'info' %> +

+

<%= content_tag(:label, l(:geocoder_options)) %> <%= text_area_tag('settings[default_geocoder_options]', @@ -15,3 +28,30 @@ :cols => 100) %>

+ + diff --git a/config/locales/de.yml b/config/locales/de.yml index 9fbb5354..114876db 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -11,7 +11,9 @@ de: label_gtt_select_icon: Icon auswählen label_parameters: Parameter label_tab_geocoder: Geocoder - geocoder_options: Geocoder Optionen + geocoder_provider: "Anbieter" + geocoder_options: Optionen + geocoder_load_example: "Load example options" gtt_settings_general_maxzoom_level: Standardwert für die maximale Zoomstufe der Karte label_default_collapsed_issues_page_map: Standardmäßig ausgeblendete Karte für Tickets @@ -56,6 +58,9 @@ de: control: geocoding: Standort-Suche geolocation: Mein Standort + search_location: "Search location" + reverse_location: "Click on the map to get location..." + search_placeholder: "Type a location..." geolocation_notification_activated: "Geolocation activated" geolocation_notification_deactivated: "Geolocation deactivated" maximize: Zoom auf alle Objekte diff --git a/config/locales/en.yml b/config/locales/en.yml index 5d1079fa..25e6e09e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -28,7 +28,9 @@ en: label_tab_general: "General" label_tab_geocoder: "Geocoder" - geocoder_options: "Geocoder Options" + geocoder_provider: "Provider" + geocoder_options: "Options" + geocoder_load_example: "Load example options" gtt_map_rotate_label: "Map rotation" gtt_map_rotate_info_html: "Hold down Shift+Alt and drag the map to rotate." @@ -100,6 +102,9 @@ en: control: geocoding: "Location search" geolocation: "My location" + search_location: "Search location" + reverse_location: "Click on the map to get location..." + search_placeholder: "Type a location..." geolocation_activated: "Geolocation activated" geolocation_deactivated: "Geolocation deactivated" maximize: "Zoom to all features" diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 8bc4aa82..8a828f13 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -28,7 +28,9 @@ ja: label_tab_general: "一般" label_tab_geocoder: "ジオコーダ" - geocoder_options: "ジオコーダのオプション" + geocoder_provider: "Provider" + geocoder_options: "オプション" + geocoder_load_example: "Load example options" gtt_map_rotate_label: "地図の回転" gtt_map_rotate_info_html: "Shift+Alt を押しながらドラッグして地図を回転します。" @@ -100,6 +102,9 @@ ja: control: geocoding: 住所検索 geolocation: 現在地へ移動 + search_location: "Search location" + reverse_location: "Click on the map to get location..." + search_placeholder: "Type a location..." geolocation_notification_activated: "Geolocation activated" geolocation_notification_deactivated: "Geolocation deactivated" maximize: 地物にズーム diff --git a/lib/redmine_gtt/hooks/view_layouts_base_html_head_hook.rb b/lib/redmine_gtt/hooks/view_layouts_base_html_head_hook.rb index f06ac494..dd3eb130 100644 --- a/lib/redmine_gtt/hooks/view_layouts_base_html_head_hook.rb +++ b/lib/redmine_gtt/hooks/view_layouts_base_html_head_hook.rb @@ -13,15 +13,19 @@ def view_layouts_base_html_head(context={}) def view_layouts_base_body_bottom(context={}) tags = []; - geocoder = {} - geocoder_options = Setting.plugin_redmine_gtt['default_geocoder_options'] - if geocoder_options.present? - begin - geocoder = JSON.parse(geocoder_options) - rescue JSON::ParserError => exception - Rails.logger.warn "Failed to parse setting's 'geocoder_options' as JSON: #{exception}\nUse default '{}' instead." - end + + geocoder = { + enabled: false + } + + if Setting.plugin_redmine_gtt['enable_geocoding_on_map'] == 'true' + geocoder = { + enabled: true, + provider: Setting.plugin_redmine_gtt['default_geocoder_provider'], + options: (JSON.parse(Setting.plugin_redmine_gtt['default_geocoder_options']) rescue {}) + } end + tags.push(tag.div :data => { :lon => Setting.plugin_redmine_gtt['default_map_center_longitude'], :lat => Setting.plugin_redmine_gtt['default_map_center_latitude'], diff --git a/package.json b/package.json index dcb51b27..084d4050 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "devDependencies": { "@types/fontfaceobserver": "^2.1.3", "@types/geojson": "^7946.0.14", + "@types/google.maps": "^3.55.9", "@types/jquery": "^3.5.29", "@types/jqueryui": "^1.12.21", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext", diff --git a/src/components/gtt-client/geocoding/CustomButtonMixin.ts b/src/components/gtt-client/geocoding/CustomButtonMixin.ts new file mode 100644 index 00000000..10575529 --- /dev/null +++ b/src/components/gtt-client/geocoding/CustomButtonMixin.ts @@ -0,0 +1,62 @@ +import ol_ext_element from 'ol-ext/util/element'; + +export function applyCustomButton(searchControl: any, options: any) { + // Remove the default button if it exists + const defaultButton = searchControl.element.querySelector('button[type="button"]'); + if (defaultButton) { + defaultButton.remove(); + } + + // Create a custom search button with a custom icon + searchControl.button = ol_ext_element.create('BUTTON', { + className: 'ol-search-gtt', + title: options.title || 'Search', + html: options.html || '', + parent: searchControl.element, + click: function () { + searchControl.element.classList.toggle('ol-collapsed'); + if (!searchControl.element.classList.contains('ol-collapsed')) { + const input = searchControl.element.querySelector('input.search'); + if (input) { + input.focus(); + searchControl.drawList_(); + } + } + }.bind(searchControl) + }) as HTMLButtonElement; + + // Handle the reverse button if reverse geocoding is enabled + if (options.providerOptions.reverse) { + // Remove the default reverse button if it exists + const defaultReverseButton = searchControl.element.querySelector('button.ol-revers'); + if (defaultReverseButton) { + defaultReverseButton.remove(); + } + + // Create a custom reverse button with a custom icon + searchControl.reverseButton = ol_ext_element.create('BUTTON', { + className: 'ol-search-gtt-reverse ol-revers', + title: options.providerOptions.reverseTitle || 'Click on the map', + html: options.html_reverse || 'X', + parent: searchControl.element, + click: function () { + if (!searchControl.get('reverse')) { + searchControl.set('reverse', !searchControl.get('reverse')); + const input = searchControl.element.querySelector('input.search'); + if (input) { + input.focus(); + searchControl.element.classList.add('ol-revers'); + } + } else { + searchControl.set('reverse', false); + } + }.bind(searchControl) + }) as HTMLButtonElement; + } + + // Move list to the end + const ul = searchControl.element.querySelector("ul.autocomplete"); + if (ul) { + searchControl.element.appendChild(ul); + } +} diff --git a/src/components/gtt-client/geocoding/SearchFactory.ts b/src/components/gtt-client/geocoding/SearchFactory.ts new file mode 100644 index 00000000..eecb6815 --- /dev/null +++ b/src/components/gtt-client/geocoding/SearchFactory.ts @@ -0,0 +1,58 @@ +// src/components/gtt-client/geocoding/SearchFactory.ts +import { applyCustomButton } from './CustomButtonMixin'; +import SearchGTT from './SearchGTT'; +import SearchGoogle from './SearchGoogle'; +import SearchNominatim from 'ol-ext/control/SearchNominatim'; +import SearchPhoton from 'ol-ext/control/SearchPhoton'; + +export function createSearchControl(options: any): any { + let searchControl: any; + + // Create search control instance based on the provider + switch (options.provider) { + // Apply settings for Nomatim provider + case 'nominatim': + options.providerOptions = { + reverse: true, // Enable reverse geocoding + typing: -1, // Disable typing delay (see Nominatim policy!) + ...options.providerOptions, + }; + searchControl = new SearchNominatim(options.providerOptions); + break; + // Apply settings for Photon provider + case 'photon': + options.providerOptions = { + // lang: 'en', // Force preferred language + reverse: true, // Enable reverse geocoding + position: true, // Priority to position + ...options.providerOptions, + }; + searchControl = new SearchPhoton(options.providerOptions); + break; + // Apply settings for Google provider + case 'google': + options.providerOptions = { + reverse: true, // Enable reverse geocoding + ...options.providerOptions, + }; + searchControl = new SearchGoogle(options.providerOptions); + break; + + case 'custom': + options.providerOptions = { + ...options.providerOptions, + }; + searchControl = new SearchGTT(options.providerOptions); + break; + // Add cases for new providers here + default: + // Throw an error if the provider is not supported + throw new Error(`Unsupported provider: ${options.provider}`); + break; + } + + // Apply custom button implementation + applyCustomButton(searchControl, options); + + return searchControl; +} diff --git a/src/components/gtt-client/geocoding/SearchGTT.ts b/src/components/gtt-client/geocoding/SearchGTT.ts new file mode 100644 index 00000000..7c489345 --- /dev/null +++ b/src/components/gtt-client/geocoding/SearchGTT.ts @@ -0,0 +1,22 @@ +// src/components/gtt-client/geocoding/SearchGTT.ts +import Search, { Options as SearchOptions } from 'ol-ext/control/Search'; + +interface SearchGTTOptions extends SearchOptions { + // Add custom options here +} + +/** + * Use this as a starting point for supporting a new geocoding service. + */ +class SearchGTT extends Search { + public button: HTMLButtonElement; + + constructor(options: SearchGTTOptions = {}) { + options = options || {}; + options.className = options.className || 'ol-search-gtt'; + + super(options); + } +} + +export default SearchGTT; diff --git a/src/components/gtt-client/geocoding/SearchGoogle.ts b/src/components/gtt-client/geocoding/SearchGoogle.ts new file mode 100644 index 00000000..8966952f --- /dev/null +++ b/src/components/gtt-client/geocoding/SearchGoogle.ts @@ -0,0 +1,198 @@ +import { transform as ol_proj_transform } from 'ol/proj.js'; +import SearchJSON, { Options as SearchOptions } from 'ol-ext/control/SearchJSON.js'; +import BaseEvent from 'ol/events/Event'; + +/** + * @typedef {Object} SearchGoogleOptions + * @property {string} apiKey Google API key + * @property {string} [language] language code + * @property {string} [region] region code, specified as a ccTLD + * @property {string} [components] specifies the component restrictions (only Geocoding) + * @property {string} [result_type] filter the results to match a specific type (only Reverse Geocoding) + * @property {string} [location_type] filter the results to match a specific location type (only Reverse Geocoding) + */ +interface SearchGoogleOptions extends SearchOptions { + apiKey: string; + language?: string; + region?: string; + components?: string; + result_type?: string; + location_type?: string; +} + +/** + * Search event + */ +export class SearchEvent extends BaseEvent { + public search: any; + public coordinate: any[]; + + constructor(type: string, search: any, coordinate: any[]) { + super(type); + this.search = search; + this.coordinate = coordinate; + } +} + +/** + * Search places using the Google Geocoding API. + * + * @constructor + * @extends {SearchJSON} + * @fires select + * @param {Object=} options Control options. + * @param {string} options.className control class name + * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. + * @param {string | undefined} options.title Title to use for the search button tooltip, default "Search" + * @param {string | undefined} options.reverseTitle Title to use for the reverse geocoding button tooltip, default "Click on the map..." + * @param {string | undefined} options.placeholder placeholder, default "Search..." + * @param {number | undefined} options.typing a delay on each typing to start searching (ms), default 1000. + * @param {integer | undefined} options.minLength minimum length to start searching, default 3 + * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 + * @param {function | undefined} options.handleResponse Handle server response to pass the features array to the list + * @param {string | undefined} options.apiKey Google API key (required) + * @param {string | undefined} options.language language code + * @param {string | undefined} options.region region code, specified as a ccTLD + * @param {string | undefined} options.components specifies the component restrictions (only Geocoding) + * @param {string | undefined} options.result_type filter the results to match a specific type (only Reverse Geocoding) + * @param {string | undefined} options.location_type filter the results to match a specific location type (only Reverse Geocoding) + */ +class SearchGoogle extends SearchJSON { + constructor(options: SearchGoogleOptions = { apiKey: null }) { + options.className = options.className || 'google'; + options.url = options.url || 'https://maps.googleapis.com/maps/api/geocode/json'; + super(options); + + if (!options.apiKey) { + throw new Error('Google Geocoding API requires an API key'); + } + + this.set('apiKey', options.apiKey); + + if (options.language) { + this.set('language', options.language); + } + if (options.region) { + this.set('region', options.region); + } + if (options.components) { + this.set('components', options.components); + } + if (options.result_type) { + this.set('result_type', options.result_type); + } + if (options.location_type) { + this.set('location_type', options.location_type); + } + } + + /** Returns the text to be displayed in the menu + * @param {any} f the feature + * @return {string} the text to be displayed in the index + * @api + */ + getTitle(f: any) { + return f.formatted_address; + } + + /** + * @param {string} s the search string + * @return {Object} request data (as key:value) + * @api + */ + requestData(s: string) { + const data: Record = { + address: s, + key: this.get('apiKey'), + }; + + const language = this.get('language'); + if (language) { + data.language = language; + } + + const region = this.get('region'); + if (region) { + data.region = region; + } + + const components = this.get('components'); + if (components) { + data.components = components; + } + + return data; + } + + /** + * Handle server response to pass the features array to the list + * @param {any} response server response + * @return {Array} an array of feature + */ + handleResponse(response: any): google.maps.GeocoderResult[] { + return response.results; + } + + /** A line has been clicked in the menu > dispatch event + * @param {google.maps.GeocoderResult} f the feature, as passed in the autocomplete + * @api + */ + select(f: any) { + var c = [f.geometry.location.lng, f.geometry.location.lat]; + // Add coordinate to the event + try { + c = ol_proj_transform(c, 'EPSG:4326', this.getMap().getView().getProjection()); + } catch (e) { /* ok */ } + this.dispatchEvent(new SearchEvent("select", f, c)); + } + + /** Reverse geocode + * @param {ol.coordinate} coord + * @api + */ + reverseGeocode(coord: any, cback: (results: google.maps.GeocoderResult[]) => void) { + const lonlat = ol_proj_transform(coord, this.getMap().getView().getProjection(), 'EPSG:4326'); + const baseUrl = this.get('url'); + + // Manually construct the query parameters to avoid double encoding the comma + let url = `${baseUrl}?latlng=${lonlat[1]},${lonlat[0]}&key=${this.get('apiKey')}`; + + const language = this.get('language'); + if (language) { + url += `&language=${encodeURIComponent(language)}`; + } + + const region = this.get('region'); + if (region) { + url += `®ion=${encodeURIComponent(region)}`; + } + + const result_type = this.get('result_type'); + if (result_type) { + url += `&result_type=${encodeURIComponent(result_type)}`; + } + + const location_type = this.get('location_type'); + if (location_type) { + url += `&location_type=${encodeURIComponent(location_type)}`; + } + + this.ajax( + url, + {}, + function (resp: any) { + if (cback) { + cback.call(this, resp.results); + } else { + if (resp && !resp.error) { + this._handleSelect(resp.results[0], true); + } + } + }.bind(this), + {} + ); + } + +} + +export default SearchGoogle; diff --git a/src/components/gtt-client/init/controls.ts b/src/components/gtt-client/init/controls.ts index 455ba69e..fb2c9a42 100644 --- a/src/components/gtt-client/init/controls.ts +++ b/src/components/gtt-client/init/controls.ts @@ -6,9 +6,9 @@ import LayerSwitcher from 'ol-ext/control/LayerSwitcher'; import Notification from 'ol-ext/control/Notification'; import { position } from 'ol-ext/control/control'; -import { setGeocoding } from "../geocoding"; import { radiansToDegrees, degreesToRadians, parseHistory } from "../helpers"; import { zoomToExtent, setGeolocation, setView, setControls, setPopover } from "../openlayers"; +import { createSearchControl } from '../geocoding/SearchFactory'; /** * Adds the toolbar and basic controls to the map instance. @@ -20,11 +20,43 @@ function addToolbarAndControls(instance: any): void { instance.map.addControl(instance.toolbar); setView.call(instance); - setGeocoding.call(instance, instance.map); + setSearchControl(instance); setGeolocation.call(instance, instance.map); parseHistory.call(instance); } +/** + * Adds the search control to the map instance. + * @param {any} map - The OpenLayers map instance. + */ +function setSearchControl(instance: any): void { + const geocoder = JSON.parse(instance.defaults.geocoder); + + // Add the search control if enabled in plugin settings + if (JSON.parse(geocoder.enabled)) { + const searchControl = createSearchControl({ + html: '', + html_reverse: '', + title: instance.i18n.control.search_location, + provider: geocoder.provider, + providerOptions: { + reverseTitle: instance.i18n.control.reverse_location, + placeholder: instance.i18n.control.search_placeholder, + ...geocoder.options + }, + }); + instance.map.addControl(searchControl); + + // Add a listener for the select event + searchControl.on('select', function(evt: any) { + instance.map.getView().animate({ + center: evt.coordinate, + zoom: Math.max(instance.map.getView().getZoom(), 18) + }); + }); + } +} + /** * Adds the FullScreen and Rotate controls to the map instance. * @param {any} instance - The GttClient instance. diff --git a/src/styles/scss/app.scss b/src/styles/scss/app.scss index f34c60fe..393b9fb4 100644 --- a/src/styles/scss/app.scss +++ b/src/styles/scss/app.scss @@ -61,8 +61,17 @@ $breakpoint-tablet: 899px; background-color: rgba(52, 73, 94, 1); } +// Unset default search icon +.ol-control.ol-search > button::before, +.ol-control.ol-search > button::after { + content: none; +} + +// Increase button icon size .ol-zoom button.ol-zoom-in, .ol-zoom button.ol-zoom-out, +.ol-search button.ol-search-gtt, +.ol-search button.ol-search-gtt-reverse, .ol-full-screen button.ol-full-screen-true, .ol-full-screen button.ol-full-screen-false, .ol-rotate button.ol-rotate-reset, @@ -70,6 +79,23 @@ $breakpoint-tablet: 899px; font-size: 1.5rem; } +.ol-search input.search { + padding: 1.0em; + font-size: 1.0rem; +} + +.ol-search ul.autocomplete li.copy { + font-size: 0.7rem; + background-color: rgba(52, 73, 94, 0.8); +} + +// Adjust search indicator animation +.ol-search.searching:before { + height: 3px; + top: 2.0em; + background: red; +} + div.ol-full-screen { right: 2.55rem; } diff --git a/tsconfig.json b/tsconfig.json index c78fe4fe..0c7237a8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es6", "module": "es6", "moduleResolution": "node", "jsx": "react", @@ -8,6 +8,10 @@ "outDir": "./dist", "allowJs": true, "noImplicitAny": true, + // "strict": true, + // "esModuleInterop": true, + // "skipLibCheck": true, + // "sourceMap": true, "paths": { "*": ["@types/*"] } diff --git a/yarn.lock b/yarn.lock index 4d744a81..fce02836 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,13 +2,6 @@ # yarn lockfile v1 -"@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0": - version "7.21.5" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz" - integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q== - dependencies: - regenerator-runtime "^0.13.11" - "@discoveryjs/json-ext@^0.5.0": version "0.5.7" resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" @@ -152,6 +145,11 @@ resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg== +"@types/google.maps@^3.55.9": + version "3.55.9" + resolved "https://registry.yarnpkg.com/@types/google.maps/-/google.maps-3.55.9.tgz#3bbe1d044d9b999392a359fb37b0de2545ac53c4" + integrity sha512-phaOMtezbT3NaXPKiI3m0OosUS7Nly0auw3Be5s/CgMWLVoDAUP1Yb/Ld0TRoRp8ibrlT4VqM5kmzfvUA0UNLQ== + "@types/jquery@*": version "3.5.16" resolved "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz" @@ -184,16 +182,9 @@ integrity sha512-At4pvmIOki8yuwLtd7BNHl3CiWNbtclUbNtScGx4OHfBd4/oWoJC8KRCIxXwkdndzhxOsPXihrsOoydxBjlE9Q== "@types/ol-ext@npm:@siedlerchr/types-ol-ext": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@siedlerchr/types-ol-ext/-/types-ol-ext-3.2.4.tgz#5411370c5cc62db6f5fddbe6c5dba203c19aeba3" - integrity sha512-d6auH1slNSyR6v2gP0J2fFPdgvRetAcThASZrcA65lciZmewRgDJaryaCj6/hjvEopokaE4KaMc1OkO8LdRNOw== - dependencies: - jspdf "^2.5.1" - -"@types/raf@^3.4.0": version "3.4.0" - resolved "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz" - integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw== + resolved "https://registry.yarnpkg.com/@siedlerchr/types-ol-ext/-/types-ol-ext-3.4.0.tgz#e57292b89fc6b00226a751479c41e78d266c283a" + integrity sha512-LtS9YNitiQ2UZV+RHVOS0Pehlkpcsp6qX9Sa2I33l4ZNqFCRB0T8YefGj0Se4SP8cPr1jc9+q1UU86BjFpg8ww== "@types/sizzle@*": version "2.3.3" @@ -391,16 +382,6 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -base64-arraybuffer@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz" - integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== - binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" @@ -423,11 +404,6 @@ browserslist@^4.21.10: node-releases "^2.0.14" update-browserslist-db "^1.0.13" -btoa@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz" - integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== - buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" @@ -438,20 +414,6 @@ caniuse-lite@^1.0.30001580: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001584.tgz#5e3ea0625d048d5467670051687655b1f7bf7dfd" integrity sha512-LOz7CCQ9M1G7OjJOF9/mzmqmj3jE/7VOmrfw6Mgs0E8cjOsbRXQJHsPBfmBOXDskXKrHLyyW3n7kpDW/4BsfpQ== -canvg@^3.0.6: - version "3.0.10" - resolved "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz" - integrity sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q== - dependencies: - "@babel/runtime" "^7.12.5" - "@types/raf" "^3.4.0" - core-js "^3.8.3" - raf "^3.4.1" - regenerator-runtime "^0.13.7" - rgbcolor "^1.0.1" - stackblur-canvas "^2.0.0" - svg-pathdata "^6.0.3" - chalk@^4.1.0: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" @@ -536,11 +498,6 @@ commander@^2.20.0: resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -core-js@^3.6.0, core-js@^3.8.3: - version "3.30.2" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz" - integrity sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg== - cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" @@ -550,13 +507,6 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -css-line-break@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz" - integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== - dependencies: - utrie "^1.0.2" - css-loader@^6.10.0: version "6.10.0" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.10.0.tgz#7c172b270ec7b833951b52c348861206b184a4b7" @@ -581,11 +531,6 @@ cssesc@^3.0.0: resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -dompurify@^2.2.0: - version "2.4.5" - resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.4.5.tgz" - integrity sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA== - earcut@^2.2.3: version "2.2.4" resolved "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz" @@ -672,11 +617,6 @@ fastest-levenshtein@^1.0.12: resolved "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== -fflate@^0.4.8: - version "0.4.8" - resolved "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz" - integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== - fill-range@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" @@ -754,14 +694,6 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -html2canvas@^1.0.0-rc.5: - version "1.4.1" - resolved "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz" - integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== - dependencies: - css-line-break "^2.1.0" - text-segmentation "^1.0.3" - icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" @@ -862,21 +794,6 @@ json-stringify-pretty-compact@^2.0.0: resolved "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz" integrity sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ== -jspdf@^2.5.1: - version "2.5.1" - resolved "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz" - integrity sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA== - dependencies: - "@babel/runtime" "^7.14.0" - atob "^2.1.2" - btoa "^1.2.1" - fflate "^0.4.8" - optionalDependencies: - canvg "^3.0.6" - core-js "^3.6.0" - dompurify "^2.2.0" - html2canvas "^1.0.0-rc.5" - kind-of@^6.0.2: version "6.0.3" resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" @@ -1038,11 +955,6 @@ pbf@3.2.1: ieee754 "^1.1.12" resolve-protobuf-schema "^2.1.0" -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" - integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== - picocolors@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" @@ -1130,13 +1042,6 @@ quickselect@^2.0.0: resolved "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz" integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== -raf@^3.4.1: - version "3.4.1" - resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz" - integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== - dependencies: - performance-now "^2.1.0" - randombytes@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" @@ -1165,11 +1070,6 @@ rechoir@^0.8.0: dependencies: resolve "^1.20.0" -regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.7: - version "0.13.11" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" @@ -1198,11 +1098,6 @@ resolve@^1.20.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -rgbcolor@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz" - integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw== - rw@^1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz" @@ -1328,11 +1223,6 @@ source-map@^0.7.4: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== -stackblur-canvas@^2.0.0: - version "2.5.0" - resolved "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz" - integrity sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ== - style-loader@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.4.tgz#f30f786c36db03a45cbd55b6a70d930c479090e7" @@ -1357,11 +1247,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -svg-pathdata@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz" - integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw== - tapable@^2.1.1, tapable@^2.2.0: version "2.2.1" resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" @@ -1388,13 +1273,6 @@ terser@^5.26.0: commander "^2.20.0" source-map-support "~0.5.20" -text-segmentation@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz" - integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== - dependencies: - utrie "^1.0.2" - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" @@ -1438,13 +1316,6 @@ util-deprecate@^1.0.2: resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -utrie@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz" - integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== - dependencies: - base64-arraybuffer "^1.0.2" - watchpack@^2.4.0: version "2.4.0" resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz"