├── .browserslistrc ├── .editorconfig ├── .env ├── .eslintrc-auto-import.json ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.txt ├── README.md ├── RELEASE-NOTES.md ├── SECURITY.md ├── deploy ├── README.md ├── gh-pages │ ├── .env │ └── deploy.sh ├── nightly-docker │ ├── .env │ ├── deploy-nightly.sh │ └── nginx │ │ └── default.conf ├── nightly-focal │ ├── .env │ ├── animet.conf │ └── deploy-nightly.sh └── nightly │ ├── .env │ ├── animet.conf │ └── deploy-nightly.sh ├── docker-compose.yml ├── index.html ├── jsconfig.json ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── scripts ├── generate_trees_layers_list.py └── wms_sources_configs.py ├── src ├── App.vue ├── assets │ ├── eccc_c_en.png │ ├── eccc_c_en.svg │ ├── eccc_c_fr.png │ ├── eccc_c_fr.svg │ ├── locations │ │ ├── loc_en.js │ │ └── loc_fr.js │ ├── parseHelper.js │ ├── presets │ │ ├── images │ │ │ ├── Alerts Current Conditions.png │ │ │ ├── Heat Waves.png │ │ │ ├── Hurricanes.png │ │ │ ├── Precipitation Type.png │ │ │ ├── Probability of Thunderstorm.png │ │ │ ├── Radar Rain.png │ │ │ ├── Radar Snow.png │ │ │ ├── Rain Accumulation.png │ │ │ ├── Sattelite Natural NightIR.png │ │ │ ├── Shrugging-Emojis.png │ │ │ ├── Significant Precipitation Type.png │ │ │ ├── Snow Accumulation.png │ │ │ ├── Temperature.png │ │ │ └── Wildfires.png │ │ └── presets.js │ └── trees │ │ ├── index.js │ │ ├── tree_en_climate.js │ │ ├── tree_en_weather.js │ │ ├── tree_fr_climate.js │ │ └── tree_fr_weather.js ├── components │ ├── Animation │ │ ├── AnimationCanvas.vue │ │ ├── AnimationConfiguration.vue │ │ ├── AnimationRectangle.vue │ │ ├── CreateAnimation.vue │ │ └── ExportAnimation.vue │ ├── Composables │ │ ├── DraggableResizable.vue │ │ ├── Tabs.vue │ │ └── TreeNode.vue │ ├── GlobalConfigs │ │ ├── AnimetLogo.vue │ │ ├── LanguageSelect.vue │ │ ├── MapCustomizations │ │ │ ├── CustomizationMenu.vue │ │ │ ├── MapPreviews.vue │ │ │ └── ProjectionHandler.vue │ │ ├── PageTheme.vue │ │ └── Share │ │ │ ├── PermaLink.vue │ │ │ └── ShareSocialLinks.vue │ ├── Layers │ │ ├── LayerConfiguration.vue │ │ ├── LayerTree.vue │ │ ├── ModelRunHandler.vue │ │ ├── OpacityHandler.vue │ │ ├── RemoveLayerHandler.vue │ │ ├── SnappedLayerHandler.vue │ │ ├── StyleHandler.vue │ │ └── VisibilityHandler.vue │ ├── Map │ │ ├── EditableTextBox.vue │ │ ├── GetFeatureInfo.vue │ │ ├── GlobalConfigs.vue │ │ ├── LegendControls.vue │ │ ├── LoadingBar.vue │ │ ├── MapCanvas.vue │ │ ├── OLControls.vue │ │ ├── SidePanel.vue │ │ └── VectorShapes.js │ └── Time │ │ ├── AnimationControls │ │ ├── ArrowControls.vue │ │ ├── ControllerOptions.vue │ │ └── PlayPauseControls.vue │ │ ├── AutoRefresh.vue │ │ ├── ErrorManager.vue │ │ ├── IntervalLocaleSelector.vue │ │ ├── LocaleSelector.vue │ │ ├── TimeControls.vue │ │ └── TimeSlider.vue ├── locales │ ├── en │ │ ├── common.json │ │ ├── layers_climate.json │ │ └── layers_weather.json │ ├── fr │ │ ├── common.json │ │ ├── layers_climate.json │ │ └── layers_weather.json │ └── importLocaleFiles.js ├── main.js ├── mixins │ └── datetimeManipulations.js ├── plugins │ ├── i18n.js │ ├── index.js │ └── vuetify.js ├── router │ └── index.js ├── stores │ ├── index.js │ └── store.js ├── utils │ ├── AxiosConfig.js │ └── IntegerAssigner.js └── views │ ├── Home.vue │ ├── MultiDisplay.vue │ └── NotFound.vue └── vite.config.mjs /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | not ie 11 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL=/msc-animet -------------------------------------------------------------------------------- /.eslintrc-auto-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "Component": true, 4 | "ComponentPublicInstance": true, 5 | "ComputedRef": true, 6 | "EffectScope": true, 7 | "ExtractDefaultPropTypes": true, 8 | "ExtractPropTypes": true, 9 | "ExtractPublicPropTypes": true, 10 | "InjectionKey": true, 11 | "PropType": true, 12 | "Ref": true, 13 | "VNode": true, 14 | "WritableComputedRef": true, 15 | "computed": true, 16 | "createApp": true, 17 | "customRef": true, 18 | "defineAsyncComponent": true, 19 | "defineComponent": true, 20 | "effectScope": true, 21 | "getCurrentInstance": true, 22 | "getCurrentScope": true, 23 | "h": true, 24 | "inject": true, 25 | "isProxy": true, 26 | "isReactive": true, 27 | "isReadonly": true, 28 | "isRef": true, 29 | "markRaw": true, 30 | "nextTick": true, 31 | "onActivated": true, 32 | "onBeforeMount": true, 33 | "onBeforeRouteLeave": true, 34 | "onBeforeRouteUpdate": true, 35 | "onBeforeUnmount": true, 36 | "onBeforeUpdate": true, 37 | "onDeactivated": true, 38 | "onErrorCaptured": true, 39 | "onMounted": true, 40 | "onRenderTracked": true, 41 | "onRenderTriggered": true, 42 | "onScopeDispose": true, 43 | "onServerPrefetch": true, 44 | "onUnmounted": true, 45 | "onUpdated": true, 46 | "provide": true, 47 | "reactive": true, 48 | "readonly": true, 49 | "ref": true, 50 | "resolveComponent": true, 51 | "shallowReactive": true, 52 | "shallowReadonly": true, 53 | "shallowRef": true, 54 | "toRaw": true, 55 | "toRef": true, 56 | "toRefs": true, 57 | "toValue": true, 58 | "triggerRef": true, 59 | "unref": true, 60 | "useAttrs": true, 61 | "useCssModule": true, 62 | "useCssVars": true, 63 | "useLink": true, 64 | "useRoute": true, 65 | "useRouter": true, 66 | "useSlots": true, 67 | "watch": true, 68 | "watchEffect": true, 69 | "watchPostEffect": true, 70 | "watchSyncEffect": true 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * .eslint.js 3 | * 4 | * ESLint configuration file. 5 | */ 6 | 7 | module.exports = { 8 | root: true, 9 | env: { 10 | node: true, 11 | }, 12 | extends: [ 13 | 'vuetify', 14 | './.eslintrc-auto-import.json', 15 | ], 16 | rules: { 17 | 'vue/multi-word-component-names': 'off', 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /generate-tree-venv 5 | /src/assets/wms_sources_configs.json 6 | _ICM_Root.crt 7 | /scripts/__pycache__ 8 | 9 | # local env files 10 | .env.local 11 | .env.*.local 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | pnpm-debug.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | 28 | # Generated files for nightly build 29 | src/assets/trees/tree_* 30 | src/locales/en/layers_* 31 | src/locales/fr/layers_* 32 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2 5 | } 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to MSC AniMet 2 | 3 | We welcome contributions to MSC AniMet, in the form of issues, bug fixes, documentation or 4 | suggestions for enhancements. This document sets out our guidelines and best 5 | practices for such contributions. 6 | 7 | It's based on the [Contributing to Open Source Projects 8 | Guide](https://contribution-guide-org.readthedocs.io/) and pygeoapi's own CONTRIBUTING.md document. 9 | 10 | MSC AniMet has the following modes of contribution: 11 | 12 | - GitHub Pull Requests 13 | 14 | 15 | ## Submitting Bugs 16 | 17 | ### Due Diligence 18 | 19 | Before submitting a bug, please do the following: 20 | 21 | * Perform __basic troubleshooting__ steps: 22 | 23 | * __Make sure you're on the latest version.__ If you're not on the most 24 | recent version, your problem may have been solved already! Upgrading is 25 | always the best first step. 26 | * [__Search the issue 27 | tracker__](https://github.com/ECCC-MSC/msc-animet/issues) 28 | to make sure it's not a known issue. 29 | 30 | ### What to put in your bug report 31 | 32 | Make sure your report gets the attention it deserves: bug reports with missing 33 | information may be ignored or punted back to you, delaying a fix. The below 34 | constitutes a bare minimum; more info is almost always better: 35 | 36 | * __What operating system are you using?__ Windows (7, 8, 10, 32-bit, 64-bit), 37 | Mac OS X, (10.7.4, 10.9.0), GNU/Linux (which distribution, which version?), iOS, Android. 38 | Again, more detail is better. 39 | * __What web browser are you using?__ Chrome, Firefox, Safari, Edge. Be specific about the version. 40 | Again, more detail is better. 41 | * __Which version or versions of the software are you using?__ Ideally, you've 42 | followed the advice above and are on the latest version, but please confirm 43 | this. 44 | * __How can the we recreate your problem?__ Imagine that we have never used 45 | MSCN AniMet before and have downloaded it for the first time. Exactly what steps 46 | do we need to take to reproduce your problem? 47 | 48 | ## Contributions and Licensing 49 | 50 | ### Contributor License Agreement 51 | 52 | Your contribution will be under our [license](https://github.com/ECCC-MSC/msc-animet/blob/main/LICENSE.txt) as per [GitHub's terms of service](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license). 53 | 54 | ### GitHub Pull Requests 55 | 56 | * Pull requests may include copyright in the source code header by the contributor if the contribution is significant or the contributor wants to claim copyright on their contribution. 57 | * All contributors shall be listed at https://github.com/ECCC-MSC/msc-animet/graphs/contributors 58 | * Unclaimed copyright, by default, is assigned to the main copyright holders as specified in https://github.com/ECCC-MSC/msc-animet/blob/main/LICENSE.txt 59 | 60 | ### Version Control Branching 61 | 62 | * Always __make a new branch__ for your work, no matter how small. This makes 63 | it easy for others to take just that one set of changes from your repository, 64 | in case you have multiple unrelated changes floating around. 65 | 66 | * __Don't submit unrelated changes in the same branch/pull request!__ If it 67 | is not possible to review your changes quickly and easily, we may reject 68 | your request. 69 | 70 | * __Base your new branch off of the appropriate branch__ on the main repository: 71 | 72 | * In general the released version of MSC AniMet is based on the ``main`` 73 | (default) branch whereas development work is done under other non-default 74 | branches. Unless you are sure that your issue affects a non-default 75 | branch, __base your branch off the ``main`` one__. 76 | 77 | * Note that depending on how long it takes for the dev team to merge your 78 | patch, the copy of ``main`` you worked off of may get out of date! 79 | * If you find yourself 'bumping' a pull request that's been sidelined for a 80 | while, __make sure you rebase or merge to latest ``main``__ to ensure a 81 | speedier resolution. 82 | 83 | 84 | ## Suggesting Enhancements 85 | 86 | We welcome suggestions for enhancements, but reserve the right to reject them 87 | if they do not follow future plans for MSC AniMet. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # develop stage 2 | FROM node:20-slim AS develop-stage 3 | RUN apt-get update || : && apt-get install python3-all python3-pip python3-owslib python3-certifi jq -y 4 | WORKDIR /app 5 | COPY package*.json ./ 6 | COPY . . 7 | 8 | # build stage 9 | FROM develop-stage AS build-stage 10 | WORKDIR /app/scripts 11 | ## cert must be copied over to app repo first 12 | ARG CERT_FILE=_ICM_Root.crt 13 | COPY $CERT_FILE /usr/local/share/ca-certificates/ 14 | RUN ln -sf /usr/local/share/ca-certificates/_ICM_Root.crt /etc/ssl/certs/_ICM_Root.pem && \ 15 | update-ca-certificates 16 | ## ENV variables from host server 17 | ARG ANIMET_NIGHTLY GEOMET_CLIMATE_NIGHTLY_URL GEOMET_WEATHER_NIGHTLY_URL GEOMET_CLIMATE_DEV_URL GEOMET_CLIMATE_STAGE_URL GEOMET_WEATHER_DEV_URL GEOMET_WEATHER_STAGE_URL GEOMET_MAPPROXY_NIGHTLY_URL VITE_SIMPLIFIED_BOUNDARIES VITE_PLACE_NAMES 18 | ENV ANIMET_NIGHTLY=${ANIMET_NIGHTLY} \ 19 | GEOMET_CLIMATE_NIGHTLY_URL=${GEOMET_CLIMATE_NIGHTLY_URL} \ 20 | GEOMET_WEATHER_NIGHTLY_URL=${GEOMET_WEATHER_NIGHTLY_URL} \ 21 | GEOMET_CLIMATE_DEV_URL=${GEOMET_CLIMATE_DEV_URL} \ 22 | GEOMET_WEATHER_DEV_URL=${GEOMET_WEATHER_DEV_URL} \ 23 | GEOMET_CLIMATE_STAGE_URL=${GEOMET_CLIMATE_STAGE_URL} \ 24 | GEOMET_WEATHER_STAGE_URL=${GEOMET_WEATHER_STAGE_URL} \ 25 | GEOMET_MAPPROXY_NIGHTLY_URL=${GEOMET_MAPPROXY_NIGHTLY_URL} \ 26 | VITE_SIMPLIFIED_BOUNDARIES=${VITE_SIMPLIFIED_BOUNDARIES} \ 27 | VITE_PLACE_NAMES=${VITE_PLACE_NAMES} 28 | RUN python3 generate_trees_layers_list.py 29 | # Extract keys from wms_sources_configs.json while ignoring "Presets" 30 | RUN config_keys=$(jq -r 'keys[] | select(. | ascii_downcase != "presets") | ascii_downcase' "/app/src/assets/wms_sources_configs.json") && \ 31 | missing_files=0 && \ 32 | for key in $config_keys; do \ 33 | for lang in en fr; do \ 34 | js_file="../src/assets/trees/tree_${lang}_${key}.js"; \ 35 | json_file="../src/locales/${lang}/layers_${key}.json"; \ 36 | if [ ! -f "$js_file" ]; then \ 37 | echo "Missing: $js_file"; \ 38 | missing_files=$((missing_files+1)); \ 39 | fi; \ 40 | if [ ! -f "$json_file" ]; then \ 41 | echo "Missing: $json_file"; \ 42 | missing_files=$((missing_files+1)); \ 43 | fi; \ 44 | done; \ 45 | done; \ 46 | # Exit if there are missing files 47 | if [ "$missing_files" -gt 0 ]; then \ 48 | echo "Warning: $missing_files files are missing!"; \ 49 | else \ 50 | echo "All expected files are generated successfully."; \ 51 | fi 52 | 53 | WORKDIR /app 54 | RUN npm install 55 | COPY deploy/nightly-docker/.env ./ 56 | RUN npm run build 57 | 58 | # production stage - Comment out this section to disable production stage 59 | # >>>>>>>>>>>>>>>>>> 60 | FROM nginx:1.23.3-alpine AS production-stage 61 | COPY --from=build-stage /app/dist/ /usr/share/nginx/html/ 62 | COPY deploy/nightly-docker/nginx/default.conf /etc/nginx/conf.d/ 63 | EXPOSE 80 64 | CMD ["nginx", "-g", "daemon off;"] 65 | # <<<<<<<<<<<<<<<<<< 66 | 67 | # production stage (without stage seperation for debugging purposes) - Uncomment out this section to disable production stage 68 | # >>>>>>>>>>>>>>>>>> 69 | # RUN apt-get install -y nginx && \ 70 | # rm -rf /var/lib/apt/lists/* && \ 71 | # cp dist/ /usr/share/nginx/html/ 72 | # COPY deploy/nightly-docker/nginx/default.conf /etc/nginx/conf.d/ 73 | # EXPOSE 80 74 | # CMD ["nginx", "-g", "daemon off;"] 75 | # <<<<<<<<<<<<<<<<<< 76 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MSC AniMet 2 | Copyright (C) 2023 Government of Canada 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MSC AniMet 2 | 3 | ## Overview 4 | 5 | MSC AniMet is a simple tool enabling users to interact with MSC Open Data weather data and create custom weather animations for any area in the world. The resulting animations can be downloaded and shared with a permalink. 6 | 7 | MSC AniMet can be used to showcase a diversity of weather events such as storms, heat waves, hurricane trajectories, air quality events, extreme wind gusts, and much more. 8 | 9 | In its current incarnation, MSC AniMet provides access to [GeoMet-Weather](https://eccc-msc.github.io/open-data/msc-geomet/readme_en/) layers. 10 | 11 | A publically available version can be found at [https://eccc-msc.github.io/msc-animet/](https://eccc-msc.github.io/msc-animet/) and the [associated documentation](https://eccc-msc.github.io/open-data/msc-animet/readme_en/) is available via the [Meteorological Service of Canada open data documentation](https://eccc-msc.github.io/open-data/). 12 | 13 | ## Installation 14 | 15 | ### Requirements 16 | 17 | - Node.js 18 | 19 | ### Dependencies 20 | 21 | Dependencies are listed in [package.json](package.json). Dependencies 22 | are automatically installed during `MSC AniMet` installation. 23 | 24 | ### Installing msc-animet 25 | 26 | ```bash 27 | # clone codebase and install 28 | git clone https://github.com/ECCC-MSC/msc-animet.git 29 | cd msc-animet 30 | npm i 31 | ``` 32 | 33 | ## Running 34 | 35 | ```bash 36 | npm run dev 37 | ``` 38 | 39 | Server will be located at http://localhost:3000/msc-animet/ 40 | 41 | Note: The path will be different if you changed the BASE_URL in the .env file 42 | 43 | ## Building for deployment 44 | 45 | ```bash 46 | npm run build 47 | ``` 48 | 49 | ## Development 50 | 51 | Bugs, enhancements and issues may be posted on [GitHub](https://github.com/ECCC-MSC/msc-animet/issues), but most are managed internally. 52 | 53 | ### Updating GeoMet-Weather layer tree names 54 | 55 | A static list of layer names and titles is generated for ease of translation and reference without needing to make a large request for the global WMS GetCapabilities document on application load. A Python script is used to update the `/src/locales/{lang}/layers` and the `/src/assets/trees/tree` json files for GeoMet Weather and GeoMet Climate. To update the layer name files, do the following (instructions for Linux): 56 | 57 | ```bash 58 | # Set directory name of your Python virtual environment 59 | PYTHON_VENV=generate-tree-venv 60 | 61 | # Create Python virtual environment 62 | python3 -m venv --system-site-packages $PYTHON_VENV 63 | 64 | # Activate virtual env 65 | . $PYTHON_VENV/bin/activate 66 | 67 | # Install deps 68 | pip install owslib 69 | 70 | # Update layers_en|fr.json 71 | cd ./scripts 72 | python3 generate_trees_layers_list.py 73 | ``` 74 | 75 | ### Adding custom WMS sources 76 | 77 | If you'd like to have your own instance of AniMet with more/other WMS sources for the layer tree, it's also possible, although for it to work they need to comply with the [OpenGIS Web Map Service (WMS) Implementation Specification](https://www.ogc.org/standard/wms/). The steps are as follows: 78 | 79 | First, on the [AniMet GitHub page](https://github.com/ECCC-MSC/msc-animet), make yourself a [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo). Once that's done, inside a terminal(commands shown are for a linux terminal), we will do the usual installation steps, except we'll be using your newly created fork instead, so: 80 | 81 | ```bash 82 | git clone https://github.com/YOUR_USERNAME/msc-animet.git 83 | cd msc-animet 84 | npm i 85 | git remote add upstream https://github.com/ECCC-MSC/msc-animet.git 86 | ``` 87 | 88 | Afterwards, we'll be changing the configuration file to specify which WMS sources we wish to have. That file is located inside `scripts/wms_sources_configs.py` and this is where we'll be adding our new sources. This process is quite simple; you simple give it a name, the url to the wms, the version and display to `True`, which would look like: 89 | 90 | ```Python 91 | wms_sources = { 92 | "Weather": { 93 | "url": "https://geo.weather.gc.ca/geomet", 94 | "version": "1.3.0" 95 | "display": True, 96 | }, 97 | "Climate": { 98 | "url": "https://geo.weather.gc.ca/geomet-climate", 99 | "version": "1.3.0" 100 | "display": True, 101 | }, 102 | "NOAA-Nowcoast": { 103 | "url": "https://nowcoast.noaa.gov/geoserver/ows", 104 | "version": "1.3.0", 105 | "query_pattern": "https://nowcoast.noaa.gov/geoserver{LAYER}/ows", 106 | "no_translations": True, 107 | "display": True, 108 | }, 109 | } 110 | ``` 111 | 112 | Optionally: 113 | 114 | - Set `no_translations` to `True` if you don't have french/english versions of these names inside both `common.json` files. 115 | - Also, if the source you are adding contains a specific pattern to query layers individually, set the `query_pattern` parameter 116 | with `{LAYER}` so the app knows how to make such queries. 117 | 118 | Once that's done and you've saved the file, inside the terminal, all we have left to do is to run the script to [update the layer tree](#updating-geomet-weather-layer-tree-names) and [run](#running) the application, it's that easy! 119 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # MSC AniMet Security Policy 2 | 3 | ## Reporting 4 | 5 | Security/vulnerability reports **should not** be submitted through GitHub issues or public discussions, but instead please send your report 6 | via the [weather.gc.ca Contact Us page](https://weather.gc.ca/mainmenu/contact_us_e.html). 7 | 8 | Please follow the [contributor guidelines](https://github.com/ECCC-MSC/msc-animet/blob/main/CONTRIBUTING.md) when submitting a vulnerability report. -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | The deployment instructions in this README is meant for internal use in ECCC-MSC servers. 4 | ## .env 5 | 6 | This `.env` file is specific to the nightly server. This file will replace the default `.env` at the root application folder via the `deploy-nightly.sh` script. 7 | 8 | ## animet.conf 9 | 10 | This is the `apache2` configuration. It enables the SPA fallback (for VueJS history mode) and ensures the path on disk is routed to the web sub-path. This apache2 configuration below seems to be the cleanest to serve the application from a `https://domain-name.ca/subpath` and to enable SPA fallback. 11 | 12 | ```conf 13 | Alias /web/sub/path /path/on/disk/to/index.html 14 | 15 | 16 | Require all granted 17 | Options Indexes FollowSymLinks 18 | AllowOverride All 19 | FallbackResource /web/sub/path 20 | 21 | ``` 22 | 23 | ## deploy-nightly.sh 24 | 25 | This is the shell script to perform a nightly build. General steps are: 26 | - `git clone` the repository into a seperate timestamped folder name 27 | - Replace default `.env` with one specific to the nightly server 28 | - `npm install` and `npm run build` 29 | - Copy the `animet.conf` file into the resulting `/dist` 30 | - The nightly server's apache config (managed by the MSC GeoMet team) is preset to `include` the `animet.conf` from that `/dist` folder. 31 | - Create a symlink called `latest` to the `/dist` from the timestamped app folder name 32 | - Cron jobs on internal CMC servers is managed by the MSC GeoMet team, which will save the raw `deploy-nightly.sh` from git repo into a `/tmp` folder, execute the script and then delete it from `/tmp` 33 | 34 | ## Manual nightly deployment 35 | 36 | This is essentially the cronjob execution: 37 | ```bash 38 | # dev Ubuntu focal 39 | curl https://raw.githubusercontent.com/ECCC-MSC/msc-animet/main/deploy/nightly-focal/deploy-nightly.sh -o /tmp/animet-deploy-nightly.sh && bash -f /tmp/animet-deploy-nightly.sh && rm -fr /tmp/animet-deploy-nightly.sh 40 | 41 | # dev Ubuntu bionic (deprecated) 42 | curl https://raw.githubusercontent.com/ECCC-MSC/msc-animet/main/deploy/nightly/deploy-nightly.sh && bash -f /tmp/animet-deploy-nightly.sh && rm -fr /tmp/animet-deploy-nightly.sh 43 | ``` 44 | 45 | ## Release management 46 | The following is done before pushing a release out to GitHub pages. 47 | - Update `package.json` version: follows (semver)[https://semver.org/] 48 | - Add an entry to release notes to https://github.com/ECCC-MSC/msc-animet/blob/main/RELEASE-NOTES.md 49 | - Follow the existing template when inputting details: 50 | - Version `x.y.z` (`YYYY-MM-DD`) 51 | - Bug Fixes, New Features, Enhancements 52 | - `git commit && git push` your changes 53 | - Create "New branch" in https://github.com/ECCC-MSC/msc-animet/branches 54 | - Name the branch based on a new x.y versioning (ie. 2.3) 55 | - Draft a new release in https://github.com/ECCC-MSC/msc-animet/releases 56 | - Tag: `x.y.z` (ie. 2.3.0); Select the prompt: "Create new tag: `x.y.z` on publish" 57 | - Target: `x.y` branch (ie. 2.3) 58 | - Previous tag: Set to the previous tag version 59 | - Release title: `x.y.z` (ie. 2.3.0) 60 | - Description: "Release version x.y.z" 61 | - Optional: Click on "Generate release notes" to add a technical list of commits 62 | - Save draft 63 | - Follow instructions on [GitHub pages deployment][#github-pages-deployment] 64 | - Once deployment is validated, publish draft release 65 | 66 | ## GitHub pages deployment 67 | 68 | Ensure the following is done first: 69 | ```bash 70 | # git clone https://github.com/ECCC-MSC/msc-animet.git or your forked repo 71 | git clone https://github.com/ECCC-MSC/msc-animet.git 72 | cd msc-animet 73 | 74 | # add remote to github repo, named "github" 75 | git remote add github https://github.com/ECCC-MSC/msc-animet.git 76 | git fetch github 77 | 78 | # Make sure you are on the x.y tagged branch 79 | git checkout -b x.y github/x.y 80 | 81 | # Install 82 | npm install 83 | 84 | # delete existing local branch called "gh-pages" 85 | git branch -D gh-pages 86 | 87 | # enable execution permission 88 | chmod +x deploy/gh-pages/deploy.sh 89 | ``` 90 | 91 | Ensure you have `git push` access to https://github.com/ECCC-MSC/msc-animet.git 92 | - You may need to set up a Personal Access Token from Github to `git push` via command line 93 | - profile -> settings -> developer settings -> personal access tokens -> generate new token -> scope: repo 94 | 95 | ```bash 96 | # if you haven't, enable the configuration for git credential.helper store 97 | git config --global credential.helper store 98 | 99 | # you'll be prompted to enter username and password for the first time you git push 100 | ``` 101 | 102 | ### npm run deploy to GH pages 103 | 104 | Once the above is setup, you can simply run: 105 | ```bash 106 | npm run deploy 107 | ``` 108 | 109 | ### Manual deploy to GH pages 110 | 111 | Optionally if something goes wrong with `npm run deploy`, run the steps manually in https://github.com/ECCC-MSC/msc-animet/blob/main/deploy/gh-pages/deploy.sh or use these manual steps to deploy: 112 | ```bash 113 | # clean new branch with no git history 114 | git checkout --orphan gh-pages 115 | 116 | # Optional: re-run npm install if dependencies are not up to date 117 | rm package-lock.json 118 | # Alternatively for a clean install 119 | rm -rf node_modules 120 | npm install 121 | 122 | # Ensure development ENV variables are unset 123 | unset ANIMET_NIGHTLY 124 | unset GEOMET_WEATHER_NIGHTLY_URL 125 | 126 | # Optional: update layer tree. Omit this step if already updated recently 127 | python3 -m venv --system-site-packages $PYTHON_VENV 128 | . $PYTHON_VENV/bin/activate 129 | pip install owslib 130 | cd ./scripts 131 | python3 generate_trees_layers_list.py 132 | deactivate 133 | cd .. 134 | rm -rf $PYTHON_VENV 135 | 136 | # Use the gh-pages .env 137 | cp ./deploy/gh-pages/.env ./.env 138 | 139 | # start build 140 | npm run build 141 | git --work-tree dist add --all 142 | git --work-tree dist commit -m 'gh-pages deploy' 143 | 144 | # push to gh-pages (deploy) 145 | git push github HEAD:gh-pages --force 146 | 147 | # delete production files 148 | rm -r dist 149 | 150 | # abandon gh-pages branch and checkout back to main branch 151 | git checkout -f main 152 | # or back to your x.y tagged branch 153 | git checkout -f x.y 154 | 155 | # delete the local gh-pages branch and leave no trace 156 | git branch -D gh-pages 157 | ``` -------------------------------------------------------------------------------- /deploy/gh-pages/.env: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL=/msc-animet -------------------------------------------------------------------------------- /deploy/gh-pages/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | PYTHON_VENV=generate-tree-venv 4 | # abort on errors 5 | 6 | set -e 7 | 8 | # Ensure DEV environment variables are unset 9 | unset ANIMET_NIGHTLY 10 | unset GEOMET_WEATHER_NIGHTLY_URL 11 | 12 | # build 13 | 14 | git checkout --orphan gh-pages 15 | echo "Setting up Python environment to generate latest layer names..." 16 | python3 -m venv --system-site-packages $PYTHON_VENV 17 | . $PYTHON_VENV/bin/activate 18 | pip install owslib 19 | cd ./scripts 20 | echo "Generating latest layer tree_en|fr_weather|climate.js ..." 21 | python3 generate_trees_layers_list.py 22 | deactivate 23 | cd .. 24 | echo "Done. Clearing Python virtual environment..." 25 | rm -rf $PYTHON_VENV 26 | echo "Use .env for gh-pages" 27 | cp ./deploy/gh-pages/.env ./.env 28 | echo "Building and bundling application files..." 29 | npm run build 30 | cp ./dist/index.html ./dist/404.html 31 | git --work-tree dist add --all 32 | git --work-tree dist commit -m 'gh-pages deploy' 33 | echo "Pushing to gh-pages..." 34 | # git remote add github https://github.com/ECCC-MSC/msc-animet.git 35 | git push github HEAD:gh-pages --force 36 | echo "Done. Clearing dist..." 37 | rm -r dist 38 | echo "Checkout back to main..." 39 | git checkout -f main 40 | echo "Deleting local gh-pages branch..." 41 | git branch -D gh-pages 42 | echo "Sucessfully deployed, check your settings" 43 | -------------------------------------------------------------------------------- /deploy/nightly-docker/.env: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL=/animet -------------------------------------------------------------------------------- /deploy/nightly-docker/deploy-nightly.sh: -------------------------------------------------------------------------------- 1 | # ================================================================= 2 | # 3 | # Author: Kevin Ngai 4 | # 5 | # Copyright (c) 2022 Kevin Ngai 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | # 28 | # ================================================================= 29 | 30 | BASEDIR=/data/web/animet-nightly 31 | GITREPO=https://github.com/ECCC-MSC/msc-animet.git 32 | DAYSTOKEEP=7 33 | 34 | # you should be okay from here 35 | 36 | DATETIME=`date +%Y%m%d` 37 | TIMESTAMP=`date +%Y%m%d.%H%M` 38 | NIGHTLYDIR=animet-$TIMESTAMP 39 | 40 | echo "Deleting AniMet nightly builds > $DAYSTOKEEP days old" 41 | 42 | cd $BASEDIR 43 | 44 | for f in `find . -type d -name "animet-20*"` 45 | do 46 | DATETIME2=`echo $f | awk -F- '{print $2}' | awk -F. '{print $1}'` 47 | let DIFF=(`date +%s -d $DATETIME`-`date +%s -d $DATETIME2`)/86400 48 | if [ $DIFF -gt $DAYSTOKEEP ]; then 49 | rm -fr $f 50 | fi 51 | done 52 | 53 | echo "Generating AniMet nightly build for $TIMESTAMP" 54 | rm -fr latest 55 | mkdir $NIGHTLYDIR && cd $NIGHTLYDIR 56 | git clone $GITREPO . -b main --depth=1 57 | 58 | # Certificate file needed for generate_trees_layers_list.py when building container 59 | CERT_FILE=/usr/local/share/ca-certificates/_ICM_Root.crt 60 | CERT_DEST_FILE=$(basename "$CERT_FILE") 61 | 62 | cp "$CERT_FILE" ./ || { echo "Failed to copy $CERT_FILE"; exit 1; } 63 | 64 | # Check if the file exists in the destination directory 65 | if [ -f "./$CERT_DEST_FILE" ]; then 66 | echo "Certificate file copied successfully: $CERT_DEST_FILE" 67 | else 68 | echo "Error: Certificate file not found in the destination after copying." 69 | exit 1 70 | fi 71 | 72 | 73 | echo "Stopping/building/starting Docker setup" 74 | docker compose --project-name msc-animet-nightly -f docker-compose.yml build --no-cache 75 | docker compose --project-name msc-animet-nightly -f docker-compose.yml down 76 | docker container rm -f msc-animet-nightly 77 | docker compose --project-name msc-animet-nightly -f docker-compose.yml up -d 78 | 79 | cat > animet-nightly.conf < 81 | ProxyPass http://localhost:5090/ 82 | ProxyPassReverse http://localhost:5090/ 83 | Require all granted 84 | 85 | EOF 86 | 87 | cd .. 88 | 89 | ln -s $NIGHTLYDIR latest 90 | chmod -R 775 $NIGHTLYDIR # ensure group writable -------------------------------------------------------------------------------- /deploy/nightly-docker/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name localhost; 5 | 6 | #access_log /var/log/nginx/host.access.log main; 7 | 8 | location / { 9 | root /usr/share/nginx/html; 10 | index index.html index.html; 11 | } 12 | 13 | error_page 404 /index.html; 14 | 15 | # redirect server error pages to the static page /50x.html 16 | # 17 | error_page 500 502 503 504 /50x.html; 18 | location = /50x.html { 19 | root /usr/share/nginx/html; 20 | } 21 | 22 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 23 | # 24 | #location ~ \.php$ { 25 | # proxy_pass http://127.0.0.1; 26 | #} 27 | 28 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 29 | # 30 | #location ~ \.php$ { 31 | # root html; 32 | # fastcgi_pass 127.0.0.1:9000; 33 | # fastcgi_index index.php; 34 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 35 | # include fastcgi_params; 36 | #} 37 | 38 | # deny access to .htaccess files, if Apache's document root 39 | # concurs with nginx's one 40 | # 41 | #location ~ /\.ht { 42 | # deny all; 43 | #} 44 | } 45 | -------------------------------------------------------------------------------- /deploy/nightly-focal/.env: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL=/animet -------------------------------------------------------------------------------- /deploy/nightly-focal/animet.conf: -------------------------------------------------------------------------------- 1 | Alias /animet /data/web/animet-nightly/latest 2 | 3 | 4 | Require all granted 5 | Options Indexes FollowSymLinks 6 | AllowOverride All 7 | FallbackResource /animet 8 | -------------------------------------------------------------------------------- /deploy/nightly-focal/deploy-nightly.sh: -------------------------------------------------------------------------------- 1 | # ================================================================= 2 | # 3 | # Author: Kevin Ngai 4 | # 5 | # Copyright (c) 2022 Kevin Ngai 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | # 28 | # ================================================================= 29 | 30 | BASEDIR=/data/web/animet-nightly 31 | GITREPO=https://github.com/ECCC-MSC/msc-animet.git 32 | DAYSTOKEEP=7 33 | 34 | # you should be okay from here 35 | 36 | DATETIME=`date +%Y%m%d` 37 | TIMESTAMP=`date +%Y%m%d.%H%M` 38 | NIGHTLYDIR=animet-$TIMESTAMP 39 | PYTHON_VENV=generate-tree-venv 40 | 41 | echo "Deleting nightly builds > $DAYSTOKEEP days old" 42 | 43 | cd $BASEDIR 44 | 45 | for f in `find . -type d -name "animet-20*"` 46 | do 47 | DATETIME2=`echo $f | awk -F- '{print $2}' | awk -F. '{print $1}'` 48 | let DIFF=(`date +%s -d $DATETIME`-`date +%s -d $DATETIME2`)/86400 49 | if [ $DIFF -gt $DAYSTOKEEP ]; then 50 | rm -fr $f 51 | fi 52 | done 53 | 54 | echo "Generating nightly build for $TIMESTAMP" 55 | rm -fr latest 56 | mkdir $NIGHTLYDIR && cd $NIGHTLYDIR 57 | git clone $GITREPO . --depth=1 58 | cp deploy/nightly-focal/.env . 59 | echo "Setting up Python environment to generate latest layer names..." 60 | python3 -m venv --system-site-packages $PYTHON_VENV 61 | . $PYTHON_VENV/bin/activate 62 | pip install owslib 63 | cd ./scripts 64 | echo "Generating latest layer tree_en|fr_weather|climate.js ..." 65 | python3 generate_trees_layers_list.py 66 | deactivate 67 | cd .. 68 | echo "Done. Clearing Python virtual environment..." 69 | rm -rf $PYTHON_VENV 70 | echo "Installing npm dependencies..." 71 | npm install 72 | echo "Building and bundling application files..." 73 | npx vue-cli-service build 74 | cp deploy/nightly-focal/animet.conf dist/ 75 | cd .. 76 | ln -s $NIGHTLYDIR/dist latest 77 | chmod -R 775 $NIGHTLYDIR # ensure group writable -------------------------------------------------------------------------------- /deploy/nightly/.env: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL=/animet/nightly/latest -------------------------------------------------------------------------------- /deploy/nightly/animet.conf: -------------------------------------------------------------------------------- 1 | Alias /animet/nightly/latest /data/web/animet-nightly/latest 2 | 3 | 4 | Require all granted 5 | Options Indexes FollowSymLinks 6 | AllowOverride All 7 | FallbackResource /animet/nightly/latest 8 | -------------------------------------------------------------------------------- /deploy/nightly/deploy-nightly.sh: -------------------------------------------------------------------------------- 1 | # ================================================================= 2 | # 3 | # Author: Kevin Ngai 4 | # 5 | # Copyright (c) 2022 Kevin Ngai 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | # 28 | # ================================================================= 29 | 30 | BASEDIR=/data/web/animet-nightly 31 | GITREPO=https://github.com/ECCC-MSC/msc-animet.git 32 | DAYSTOKEEP=7 33 | 34 | # you should be okay from here 35 | 36 | DATETIME=`date +%Y%m%d` 37 | TIMESTAMP=`date +%Y%m%d.%H%M` 38 | NIGHTLYDIR=animet-$TIMESTAMP 39 | PYTHON_VENV=generate-tree-venv 40 | 41 | echo "Deleting nightly builds > $DAYSTOKEEP days old" 42 | 43 | cd $BASEDIR 44 | 45 | for f in `find . -type d -name "animet-20*"` 46 | do 47 | DATETIME2=`echo $f | awk -F- '{print $2}' | awk -F. '{print $1}'` 48 | let DIFF=(`date +%s -d $DATETIME`-`date +%s -d $DATETIME2`)/86400 49 | if [ $DIFF -gt $DAYSTOKEEP ]; then 50 | rm -fr $f 51 | fi 52 | done 53 | 54 | echo "Generating nightly build for $TIMESTAMP" 55 | rm -fr latest 56 | mkdir $NIGHTLYDIR && cd $NIGHTLYDIR 57 | git clone $GITREPO . --depth=1 58 | cp deploy/nightly/.env . 59 | echo "Setting up Python environment to generate latest layer names..." 60 | python3 -m venv --system-site-packages $PYTHON_VENV 61 | . $PYTHON_VENV/bin/activate 62 | pip install owslib 63 | cd ./scripts 64 | echo "Generating latest layer tree_en|fr_weather|climate.js ..." 65 | python3 generate_trees_layers_list.py 66 | deactivate 67 | cd .. 68 | echo "Done. Clearing Python virtual environment..." 69 | rm -rf $PYTHON_VENV 70 | echo "Installing npm dependencies..." 71 | npm install 72 | echo "Building and bundling application files..." 73 | npx vue-cli-service build 74 | cp deploy/nightly/animet.conf dist/ 75 | cd .. 76 | ln -s $NIGHTLYDIR/dist latest 77 | # ensure usergroup is dmsec 78 | chgrp dmsec -R $NIGHTLYDIR 79 | chmod -R 775 $NIGHTLYDIR # ensure group writable -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # for local development 2 | services: 3 | msc-animet: 4 | image: eccc-msc/msc-animet:nightly 5 | container_name: msc-animet-nightly 6 | build: 7 | context: . 8 | args: 9 | ANIMET_NIGHTLY: 'True' 10 | GEOMET_CLIMATE_NIGHTLY_URL: "${GEOMET_CLIMATE_NIGHTLY_URL}" 11 | GEOMET_WEATHER_NIGHTLY_URL: "${GEOMET_WEATHER_NIGHTLY_URL}" 12 | GEOMET_CLIMATE_DEV_URL: "${GEOMET_CLIMATE_DEV_URL}" 13 | GEOMET_WEATHER_DEV_URL: "${GEOMET_WEATHER_DEV_URL}" 14 | GEOMET_CLIMATE_STAGE_URL: "${GEOMET_CLIMATE_STAGE_URL}" 15 | GEOMET_WEATHER_STAGE_URL: "${GEOMET_WEATHER_STAGE_URL}" 16 | GEOMET_MAPPROXY_NIGHTLY_URL: "${GEOMET_MAPPROXY_NIGHTLY_URL}" 17 | VITE_SIMPLIFIED_BOUNDARIES: "${VITE_SIMPLIFIED_BOUNDARIES}" 18 | VITE_PLACE_NAMES: "${VITE_PLACE_NAMES}" 19 | restart: unless-stopped 20 | ports: 21 | - '5090:80' 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | MSC AniMet 14 | 15 | 16 | 17 | We're sorry but this app doesn't work properly without JavaScript 19 | enabled. Please enable it to continue. 21 | 22 | 23 | 24 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "target": "es5", 5 | "module": "esnext", 6 | "baseUrl": "./", 7 | "moduleResolution": "bundler", 8 | "paths": { 9 | "@/*": [ 10 | "src/*" 11 | ] 12 | }, 13 | "lib": [ 14 | "esnext", 15 | "dom", 16 | "dom.iterable", 17 | "scripthost" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msc-animet", 3 | "version": "2.3.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview", 8 | "lint": "eslint . --fix --ignore-path .gitignore", 9 | "deploy": "./deploy/gh-pages/deploy.sh" 10 | }, 11 | "dependencies": { 12 | "@mdi/font": "7.4.47", 13 | "axios": "^1.7.2", 14 | "axios-retry": "^4.4.2", 15 | "canvas-txt": "^3.0.0", 16 | "core-js": "^3.37.1", 17 | "countries-and-timezones": "^3.6.0", 18 | "h264-mp4-encoder": "^1.0.12", 19 | "lodash": "^4.17.21", 20 | "luxon": "^3.4.4", 21 | "mitt": "^3.0.1", 22 | "ol": "^9.2.4", 23 | "ol-contextmenu": "^5.5.0", 24 | "proj4": "^2.11.0", 25 | "vite-plugin-node-polyfills": "^0.22.0", 26 | "vue": "^3.4.31", 27 | "vue-i18n": "^9.13.1", 28 | "vuetify": "^3.6.11" 29 | }, 30 | "devDependencies": { 31 | "@vitejs/plugin-vue": "^5.0.5", 32 | "eslint": "^8.57.0", 33 | "eslint-config-standard": "^17.1.0", 34 | "eslint-config-vuetify": "^1.0.0", 35 | "eslint-plugin-import": "^2.29.1", 36 | "eslint-plugin-n": "^16.6.2", 37 | "eslint-plugin-node": "^11.1.0", 38 | "eslint-plugin-promise": "^6.4.0", 39 | "eslint-plugin-vue": "^9.27.0", 40 | "pinia": "^2.1.7", 41 | "prettier": "^3.3.3", 42 | "sass": "1.77.6", 43 | "unplugin-auto-import": "^0.17.6", 44 | "unplugin-fonts": "^1.1.1", 45 | "unplugin-vue-components": "^0.27.2", 46 | "unplugin-vue-router": "^0.10.0", 47 | "vite": "^5.3.3", 48 | "vite-plugin-vue-layouts": "^0.11.0", 49 | "vite-plugin-vuetify": "^2.0.3", 50 | "vue-router": "^4.4.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/public/favicon.ico -------------------------------------------------------------------------------- /scripts/wms_sources_configs.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | wms_sources = { 4 | "Presets": { 5 | "url": os.environ.get("GEOMET_WEATHER_NIGHTLY_URL", default="https://geo.weather.gc.ca/geomet"), 6 | "version": "1.3.0", 7 | "display": True, 8 | }, 9 | "Weather": { 10 | "url": "https://geo.weather.gc.ca/geomet", 11 | "version": "1.3.0", 12 | "display": True, 13 | }, 14 | "Climate": { 15 | "url": "https://geo.weather.gc.ca/geomet-climate", 16 | "version": "1.3.0", 17 | "display": True, 18 | }, 19 | "WeatherNightly": { 20 | "url": os.environ.get("GEOMET_WEATHER_NIGHTLY_URL", default=""), 21 | "version": "1.3.0", 22 | "display": os.environ.get("ANIMET_NIGHTLY", default=False), 23 | "source_validation": True, 24 | }, 25 | "ClimateNightly": { 26 | "url": os.environ.get("GEOMET_CLIMATE_NIGHTLY_URL", default=""), 27 | "version": "1.3.0", 28 | "display": os.environ.get("ANIMET_NIGHTLY", default=False), 29 | "source_validation": True, 30 | }, 31 | "WeatherDev": { 32 | "url": os.environ.get("GEOMET_WEATHER_DEV_URL", default=""), 33 | "version": "1.3.0", 34 | "display": os.environ.get("ANIMET_NIGHTLY", default=False), 35 | "source_validation": True, 36 | }, 37 | "ClimateDev": { 38 | "url": os.environ.get("GEOMET_CLIMATE_DEV_URL", default=""), 39 | "version": "1.3.0", 40 | "display": os.environ.get("ANIMET_NIGHTLY", default=False), 41 | "source_validation": True, 42 | }, 43 | "WeatherStage": { 44 | "url": os.environ.get("GEOMET_WEATHER_STAGE_URL", default=""), 45 | "version": "1.3.0", 46 | "display": os.environ.get("ANIMET_NIGHTLY", default=False), 47 | "source_validation": True, 48 | }, 49 | "ClimateStage": { 50 | "url": os.environ.get("GEOMET_CLIMATE_STAGE_URL", default=""), 51 | "version": "1.3.0", 52 | "display": os.environ.get("ANIMET_NIGHTLY", default=False), 53 | "source_validation": True, 54 | }, 55 | "MapProxy": { 56 | "url": os.environ.get("GEOMET_MAPPROXY_NIGHTLY_URL", default=""), 57 | "version": "1.3.0", 58 | "no_translations": True, 59 | "display": os.environ.get("ANIMET_NIGHTLY", default=False), 60 | "source_validation": True, 61 | }, 62 | "ECMWF": { 63 | "url": "https://eccharts.ecmwf.int/wms/?token=public", 64 | "version": "1.3.0", 65 | "display": True, 66 | }, 67 | "NOAA - nowCOAST": { 68 | "url": "https://nowcoast.noaa.gov/geoserver/ows", 69 | "version": "1.3.0", 70 | "query_pattern": "https://nowcoast.noaa.gov/geoserver{LAYER}/ows", 71 | "no_translations": True, 72 | "display": True, 73 | }, 74 | "NOAA - NCEP": { 75 | "url": "https://opengeo.ncep.noaa.gov/geoserver/ows", 76 | "version": "1.3.0", 77 | "query_pattern": "https://opengeo.ncep.noaa.gov/geoserver{LAYER}/ows", 78 | "no_translations": True, 79 | "display": True, 80 | }, 81 | "NASA": { 82 | "url": "https://gibs.earthdata.nasa.gov/wms/epsg3857/best/wms.cgi", 83 | "version": "1.3.0", 84 | "display": True, 85 | }, 86 | "GEBCO": { 87 | "url": "https://wms.gebco.net/mapserv", 88 | "version": "1.3.0", 89 | "no_translations": True, 90 | "display": True, 91 | }, 92 | "NRCan": { 93 | "url": "https://maps.geogratis.gc.ca/wms/canvec_en", 94 | "url_fr": "https://maps.geogratis.gc.ca/wms/canvec_fr", 95 | "version": "1.3.0", 96 | "display": True, 97 | }, 98 | "MRNFTerritories": { 99 | "url": "https://servicescarto.mern.gouv.qc.ca/pes/services/Territoire/SDA_WMS/MapServer/WmsServer", 100 | "version": "1.3.0", 101 | "display": True, 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 45 | 46 | 59 | 60 | 89 | -------------------------------------------------------------------------------- /src/assets/eccc_c_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/eccc_c_en.png -------------------------------------------------------------------------------- /src/assets/eccc_c_fr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/eccc_c_fr.png -------------------------------------------------------------------------------- /src/assets/locations/loc_en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | loc_en : [ 3 | { 4 | "Title": "Quebec", 5 | "Name": "qc", 6 | "isLeaf": false, 7 | "children": [ 8 | { 9 | "Title": "Montreal", 10 | "Name": "montreal_qc", 11 | "Extent": [-8280283.303890682, 5678857.571280391, -8108827.659879656, 5734308.943858343], 12 | "isLeaf": true 13 | } 14 | ] 15 | }, 16 | { 17 | "Title": "Ontario", 18 | "Name": "on", 19 | "isLeaf": false, 20 | "children": [ 21 | { 22 | "Title": "Toronto", 23 | "Name": "toronto_on", 24 | "Extent": [-8969547.012868665, 5382021.668120905, -8733221.139226034, 5458453.063218779], 25 | "isLeaf": true 26 | } 27 | ] 28 | }, 29 | { 30 | "Title": "British Columbia", 31 | "Name": "bc", 32 | "isLeaf": false, 33 | "children": [ 34 | { 35 | "Title": "Vancouver", 36 | "Name": "vancouver_bc", 37 | "Extent": [-13814503.767981488, 6272314.226027918, -13556133.788074996, 6355875.0215992285], 38 | "isLeaf": true 39 | } 40 | ] 41 | }, 42 | ] 43 | } -------------------------------------------------------------------------------- /src/assets/locations/loc_fr.js: -------------------------------------------------------------------------------- 1 | export default { 2 | loc_fr : [ 3 | { 4 | "Title": "Québec", 5 | "Name": "qc", 6 | "isLeaf": false, 7 | "children": [ 8 | { 9 | "Title": "Montréal", 10 | "Name": "montreal_qc", 11 | "Extent": [-8280283.303890682, 5678857.571280391, -8108827.659879656, 5734308.943858343], 12 | "isLeaf": true 13 | } 14 | ] 15 | }, 16 | { 17 | "Title": "Ontario", 18 | "Name": "on", 19 | "isLeaf": false, 20 | "children": [ 21 | { 22 | "Title": "Toronto", 23 | "Name": "toronto_on", 24 | "Extent": [-8969547.012868665, 5382021.668120905, -8733221.139226034, 5458453.063218779], 25 | "isLeaf": true 26 | } 27 | ] 28 | }, 29 | { 30 | "Title": "Colombie britannique", 31 | "Name": "bc", 32 | "isLeaf": false, 33 | "children": [ 34 | { 35 | "Title": "Vancouver", 36 | "Name": "vancouver_bc", 37 | "Extent": [-13814503.767981488, 6272314.226027918, -13556133.788074996, 6355875.0215992285], 38 | "isLeaf": true 39 | } 40 | ] 41 | }, 42 | ] 43 | } -------------------------------------------------------------------------------- /src/assets/parseHelper.js: -------------------------------------------------------------------------------- 1 | let durationRegex = 2 | /^(-)?P(?:(?:(\d+\.?\d*)Y)?(?:(\d+\.?\d*)M)?(?:(\d+\.?\d*)D)?(?:T(?:(\d+\.?\d*)H)?(?:(\d+\.?\d*)M)?(?:(\d+\.?\d*)S)?)?|(\d+\.?\d*)W)$/ 3 | /** 4 | * Parse ISO 8601 duration (with a few limitations) 5 | * 6 | * @example 7 | * let aDayAgo = parseDuration('-P1D').add, yesterday = aDayAgo(new Date()); 8 | * 9 | * @param {String} duration: ISO 8601 duration, only in "PnYnMnDTnHnMnS" or "PnW" formats, n being an integer 10 | * @throws {Error} When duration cannot be parsed 11 | * @returns {Object} Parsed duration with "add" method that sums or substracts parsed duration to a given date, accorging duration sign 12 | */ 13 | export default function parseDuration(duration) { 14 | let parsed 15 | duration && 16 | duration.replace(durationRegex, (_, sign, ...units) => { 17 | sign = sign ? -1 : 1 18 | // parse number for each unit 19 | let [year, month, day, hour, minute, second, week] = units.map( 20 | (num) => parseInt(num, 10) * sign || 0, 21 | ) 22 | parsed = { year, month, week, day, hour, minute, second } 23 | }) 24 | // no regexp match 25 | if (!parsed) { 26 | throw new Error(`Invalid duration "${duration}"`) 27 | } 28 | 29 | return Object.assign(parsed, { 30 | /** 31 | * Sum or substract parsed duration to date 32 | * 33 | * @param {Date} date: Any valid date 34 | * @throws {TypeError} When date is not valid 35 | * @returns {Date} New date with duration difference 36 | */ 37 | add(date) { 38 | if ( 39 | Object.prototype.toString.call(date) !== '[object Date]' || 40 | isNaN(date.valueOf()) 41 | ) { 42 | throw new TypeError('Invalide date') 43 | } 44 | return new Date( 45 | Date.UTC( 46 | date.getUTCFullYear() + parsed.year, 47 | date.getUTCMonth() + parsed.month, 48 | date.getUTCDate() + parsed.day + parsed.week * 7, 49 | date.getUTCHours() + parsed.hour, 50 | date.getUTCMinutes() + parsed.minute, 51 | date.getUTCSeconds() + parsed.second, 52 | date.getUTCMilliseconds(), 53 | ), 54 | ) 55 | }, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/assets/presets/images/Alerts Current Conditions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/presets/images/Alerts Current Conditions.png -------------------------------------------------------------------------------- /src/assets/presets/images/Heat Waves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/presets/images/Heat Waves.png -------------------------------------------------------------------------------- /src/assets/presets/images/Hurricanes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/presets/images/Hurricanes.png -------------------------------------------------------------------------------- /src/assets/presets/images/Precipitation Type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/presets/images/Precipitation Type.png -------------------------------------------------------------------------------- /src/assets/presets/images/Probability of Thunderstorm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/presets/images/Probability of Thunderstorm.png -------------------------------------------------------------------------------- /src/assets/presets/images/Radar Rain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/presets/images/Radar Rain.png -------------------------------------------------------------------------------- /src/assets/presets/images/Radar Snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/presets/images/Radar Snow.png -------------------------------------------------------------------------------- /src/assets/presets/images/Rain Accumulation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/presets/images/Rain Accumulation.png -------------------------------------------------------------------------------- /src/assets/presets/images/Sattelite Natural NightIR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/presets/images/Sattelite Natural NightIR.png -------------------------------------------------------------------------------- /src/assets/presets/images/Shrugging-Emojis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/presets/images/Shrugging-Emojis.png -------------------------------------------------------------------------------- /src/assets/presets/images/Significant Precipitation Type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/presets/images/Significant Precipitation Type.png -------------------------------------------------------------------------------- /src/assets/presets/images/Snow Accumulation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/presets/images/Snow Accumulation.png -------------------------------------------------------------------------------- /src/assets/presets/images/Temperature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/presets/images/Temperature.png -------------------------------------------------------------------------------- /src/assets/presets/images/Wildfires.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ECCC-MSC/msc-animet/0a65bbf998a2ac71875315e5e95ba029ff6a4e8f/src/assets/presets/images/Wildfires.png -------------------------------------------------------------------------------- /src/assets/trees/index.js: -------------------------------------------------------------------------------- 1 | const modules = {} 2 | const files = import.meta.glob(['./*.js', '../presets/*.js'], { eager: true }) 3 | 4 | Object.keys(files).forEach((key) => { 5 | if (key === './index.js') return 6 | const fileName = key.split('/').pop().replace('.js', '') 7 | modules[fileName] = files[key].default 8 | }) 9 | 10 | export default modules 11 | -------------------------------------------------------------------------------- /src/components/Animation/ExportAnimation.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ $t('MP4ExportTitle') }} 5 | 6 | 7 | {{ mp4URL ? $t('MP4ExportSubtitle') : $t('JPEGExportSubtitle') }} 8 | 9 | 10 | 11 | 19 | 25 | 26 | 27 | 28 | 35 | {{ mp4URL ? $t('MP4ExportDownload') : $t('JPEGExportDownload') }} [{{ 36 | this.formatBytes(this.outputSize, 0) 37 | }}] 38 | mdi-download 39 | 40 | 41 | 42 | 43 | 44 | 45 | 110 | 111 | 123 | -------------------------------------------------------------------------------- /src/components/Composables/DraggableResizable.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 114 | 115 | 133 | -------------------------------------------------------------------------------- /src/components/Composables/Tabs.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 24 | {{ sourceParameters.no_translations ? tab : $t(tab) }} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 37 | 38 | 44 | 45 | {{ 46 | sourceParameters.no_translations ? item : $t(item) 47 | }} 48 | 49 | 50 | 51 | 52 | 59 | 60 | 61 | 62 | 63 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 230 | 231 | 338 | -------------------------------------------------------------------------------- /src/components/Composables/TreeNode.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | {{ node[`Title_${$i18n.locale}`] }} 10 | 11 | 12 | 13 | 14 | 15 | {{ node.isOpen ? 'mdi-menu-down' : 'mdi-menu-right' }} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {{ node[props.titleProp] }} 24 | 25 | 26 | 27 | 28 | 32 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 179 | 180 | 242 | -------------------------------------------------------------------------------- /src/components/GlobalConfigs/AnimetLogo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ $t("MSCAnimet") }} 4 | 5 | 12 | 19 | 20 | 21 | 22 | 23 | 41 | 42 | 98 | -------------------------------------------------------------------------------- /src/components/GlobalConfigs/LanguageSelect.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | {{ this.getFlagLang }} 10 | 11 | 12 | 13 | 14 | 47 | 56 | -------------------------------------------------------------------------------- /src/components/GlobalConfigs/MapCustomizations/CustomizationMenu.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | mdi-earth 15 | 16 | 17 | {{ $t('MapCustomizations') }} 18 | 19 | 20 | 21 | 22 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 97 | 98 | 123 | 124 | 147 | -------------------------------------------------------------------------------- /src/components/GlobalConfigs/MapCustomizations/ProjectionHandler.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | {{ item.value.split(':')[1] }} 16 | {{ `${$t(item.value.replace(':', ''))}` }} 17 | 18 | 19 | 20 | 21 | {{ item.value.split(':')[1] }} 22 | {{ `${$t(item.value.replace(':', ''))}` }} 23 | 24 | 25 | 26 | 27 | 101 | 102 | 107 | -------------------------------------------------------------------------------- /src/components/GlobalConfigs/PageTheme.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 15 | mdi-theme-light-dark 16 | 17 | 18 | 19 | {{ 20 | isDark ? t('MP4CreateLightMode') : t('MP4CreateDarkMode') 21 | }} 22 | 23 | 24 | 25 | 26 | 72 | 73 | 88 | -------------------------------------------------------------------------------- /src/components/GlobalConfigs/Share/PermaLink.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | mdi-share 12 | {{ $t('Share') }} 13 | 14 | 15 | 16 | 17 | {{ $t('Share') }} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 37 | 38 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 257 | 258 | 263 | 264 | 276 | -------------------------------------------------------------------------------- /src/components/GlobalConfigs/Share/ShareSocialLinks.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | {{ platform.icon }} 12 | 13 | 14 | 15 | 16 | 105 | -------------------------------------------------------------------------------- /src/components/Layers/LayerConfiguration.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 21 | 22 | {{ $t(item.get('layerName').split(' ')[0]) }} 23 | 24 | 25 | {{ item.get('layerName') }} 26 | 27 | 36 | 37 | 38 | 39 | 40 | 43 | {{ $t(item.get('layerName').split(' ')[0]) }} 44 | 45 | 46 | {{ item.get('layerName') }} 47 | 48 | 49 | 50 | 51 | 52 | 53 | 57 | 61 | 65 | 69 | 73 | 74 | 75 | 76 | 77 | 81 | 88 | 89 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 154 | 155 | 224 | -------------------------------------------------------------------------------- /src/components/Layers/ModelRunHandler.vue: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | 24 | 25 | {{ localeDateFormat(item.raw, null, 'DATETIME_MED') }} 26 | 27 | 28 | 29 | 30 | 110 | 111 | 119 | 120 | 125 | -------------------------------------------------------------------------------- /src/components/Layers/OpacityHandler.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{ $t('LayerBarOpacity') }} 19 | 20 | 21 | {{ Math.ceil(Math.round(item.get('opacity') * 100)) + '%' }} 22 | 23 | 24 | 25 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 61 | 62 | 67 | -------------------------------------------------------------------------------- /src/components/Layers/RemoveLayerHandler.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | {{ $t('LayerBarRemoveTooltip') }} 16 | 17 | 18 | 19 | 38 | 39 | 44 | -------------------------------------------------------------------------------- /src/components/Layers/SnappedLayerHandler.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | 21 | 22 | 23 | {{ $t('SnappedLayer') }} 24 | 25 | 26 | 27 | 28 | {{ $t('SnapLayerToExtent') }} 29 | 30 | 31 | 32 | 37 | {{ $t('LayerBarCurrentTooltip') }} : 38 | {{ 39 | localeDateFormat( 40 | item.get('layerDateArray')[item.get('layerDateIndex')], 41 | item.get('layerTimeStep'), 42 | ) 43 | }} 44 | 45 | 46 | {{ $t('LayerBarStartsTooltip') }} : 47 | {{ 48 | localeDateFormat( 49 | item.get('layerStartTime'), 50 | item.get('layerTimeStep'), 51 | ) 52 | }} 53 | 54 | 55 | {{ $t('LayerBarEndsTooltip') }} : 56 | {{ 57 | localeDateFormat(item.get('layerEndTime'), item.get('layerTimeStep')) 58 | }} 59 | 60 | 61 | {{ $t('LayerBarStepTooltip') }} : 62 | {{ item.get('layerTrueTimeStep') }} 63 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 76 | 77 | 78 | {{ $t('NoTimeTooltip') }} 79 | 80 | 81 | 82 | 116 | 117 | 125 | -------------------------------------------------------------------------------- /src/components/Layers/StyleHandler.vue: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 26 | 27 | 28 | {{ $t('LayerStyle') }} 29 | 30 | 31 | 32 | toggleLegends(item.get('layerName'), value) 41 | " 42 | :label="$t('DisplayLegend')" 43 | > 44 | 45 | {{ $t('DisplayLegend') }} 46 | 47 | 48 | 53 | 60 | 61 | 62 | 63 | mdi-check-circle-outline 64 | 65 | 66 | 67 | 68 | {{ style.Name }} 69 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 149 | 150 | 186 | -------------------------------------------------------------------------------- /src/components/Layers/VisibilityHandler.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | {{ $t('LayerBarVisibilityTooltip') }} 17 | 18 | 22 | 23 | {{ $t('LayerBarInvisibleTooltip') }} 24 | 25 | 26 | {{ $t('LayerBarMapTime') }} 27 | {{ 28 | localeDateFormat( 29 | mapTimeSettings.Extent[mapTimeSettings.DateIndex], 30 | mapTimeSettings.Step, 31 | ) 32 | }} 33 | 34 | 35 | {{ $t('LayerBarMissingTimestep') }} 36 | 37 | 38 | {{ $t('LayerBarClosestTime') }} 39 | {{ 40 | localeDateFormat( 41 | item.get('layerDateIndex') === -1 42 | ? item.get('layerStartTime') 43 | : item.get('layerEndTime'), 44 | item.get('layerTimeStep'), 45 | ) 46 | }} 47 | 48 | 49 | 50 | 51 | 52 | 117 | 118 | 123 | -------------------------------------------------------------------------------- /src/components/Map/EditableTextBox.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 22 | 23 | 24 | 25 | 26 | 103 | 104 | 152 | -------------------------------------------------------------------------------- /src/components/Map/GlobalConfigs.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | mdi-information-outline 28 | 29 | 30 | {{ $t('UserDoc') }} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 43 | 54 | -------------------------------------------------------------------------------- /src/components/Map/LegendControls.vue: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 22 | 26 | 27 | 28 | 29 | 30 | 138 | 139 | 179 | -------------------------------------------------------------------------------- /src/components/Map/LoadingBar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 83 | -------------------------------------------------------------------------------- /src/components/Map/OLControls.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | mdi-plus 20 | 21 | 22 | 39 | mdi-minus 40 | 41 | 42 | 43 | 44 | 77 | 78 | 112 | -------------------------------------------------------------------------------- /src/components/Time/AnimationControls/ArrowControls.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 100 | -------------------------------------------------------------------------------- /src/components/Time/AnimationControls/ControllerOptions.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | mdi-cog 15 | 16 | 17 | 18 | 19 | 20 | {{ $t('ControllerOptions') }} 21 | 22 | 32 | 33 | 39 | {{ $t(action) }} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 83 | 84 | 110 | -------------------------------------------------------------------------------- /src/components/Time/AnimationControls/PlayPauseControls.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 26 | {{ changeIcon }} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 205 | 206 | 220 | -------------------------------------------------------------------------------- /src/components/Time/AutoRefresh.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 146 | -------------------------------------------------------------------------------- /src/components/Time/IntervalLocaleSelector.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | {{ formatDuration(item.raw) }} 20 | 21 | 22 | 23 | 24 | 25 | 35 | 36 | 37 | 38 | 39 | 40 | {{ formatDuration(item.raw) }} 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 112 | 113 | 127 | -------------------------------------------------------------------------------- /src/components/Time/LocaleSelector.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 25 | 26 | 27 | 32 | 33 | 48 | 55 | 56 | 57 | 58 | 59 | 68 | 69 | 70 | {{ $t('RevertTimeZone') }} 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 269 | 270 | 317 | -------------------------------------------------------------------------------- /src/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "AddArrow": "Arrow", 3 | "AddBox": "Rectangle", 4 | "AddCircle": "Circle", 5 | "AddPolygon": "Polygon", 6 | "AddPreset": "Add preset", 7 | "AddTextbox": "Textbox", 8 | "AnimationFrom": "Weather animation from", 9 | "AppBarCreate": "Animation configuration", 10 | "AppBarExport": "Created animation", 11 | "AppBarMap": "Map", 12 | "ApplyColor": "Apply color to basemap", 13 | "AspectSelection": "Select aspect ratio", 14 | "AttributionOSM": "Map by ©OpenStreetMap", 15 | "Base": "Base", 16 | "Black": "Black", 17 | "Boundaries": "Boundaries", 18 | "BrokenLayer": "Currently unable to load layer.", 19 | "Climate": "GeoMet-Climate", 20 | "ClimateDev": "GeoMet-Climate Dev", 21 | "ClimateNightly": "GeoMet-Climate Nightly", 22 | "ClimateStage": "GeoMet-Climate Stage", 23 | "Close": "Close", 24 | "CMD_WMS": "CMD-WMS", 25 | "Colon": ":", 26 | "ColorBorder": "Legends with coloured borders", 27 | "ControllerOptions": "Playback options", 28 | "Dark": "Dark", 29 | "DisplayLegend": "Display legend", 30 | "DocumentationURL": "https://eccc-msc.github.io/open-data/msc-animet/readme_en/", 31 | "ECMWF": "ECMWF - ecCharts", 32 | "ElementsOutOfFrame": "Legends and text boxes must be placed inside the frame to appear in the output animation.", 33 | "EPSG3857": "Web Mercator", 34 | "EPSG3978": "Lambert Conic Conformal", 35 | "EPSG3995": "Arctic Polar Stereo", 36 | "EPSG4326": "WGS84 (lat/lon)", 37 | "ExpiredTimesteps": "Some timesteps have expired. The temporal values have been updated.", 38 | "ExtentFullyOOB": "The entire selected temporal extent is no longer available. The date range slider has been reset.", 39 | "Fill": "Fill", 40 | "FPS": "FPS", 41 | "FramesPerSecond": "Frames per second", 42 | "GoToHomePage": "Go to MSC AniMet home page", 43 | "Grey": "Grey", 44 | "InvalidSRS": "CRS {CRS} is invalid for this data source.", 45 | "JPEGCreateButtonLabel": "Create image", 46 | "JPEGExportDownload": "Download as JPEG", 47 | "JPEGExportSubtitle": "Preview the created image and download using the button below", 48 | "LayerBarClosestTime": "Closest time: ", 49 | "LayerBarCurrentTooltip": "Current", 50 | "LayerBarEndsTooltip": "End", 51 | "LayerBarInvisibleTooltip": "Closest date out of bounds. Layer not shown.", 52 | "LayerBarMapTime": "Current map time: ", 53 | "LayerBarMissingTimestep": "The current map time is in-between this layer's available timesteps.", 54 | "LayerBarOpacity": "Opacity", 55 | "LayerBarRemoveTooltip": "Remove layer", 56 | "LayerBarStartsTooltip": "Start", 57 | "LayerBarStepTooltip": "Interval", 58 | "LayerBarVisibilityTooltip": "Toggle visibility", 59 | "LayerControlsTitle": "Configure", 60 | "LayerStyle": "Select layer style", 61 | "LayerTree": "Add", 62 | "Light": "Light", 63 | "Loading": "Loading...", 64 | "LocalTime": "Local", 65 | "Loop": "Loop", 66 | "LoopRetry": "Service unavailable. Retrying in {SECONDS} seconds.", 67 | "MadeWithAniMet": "Made with MSC AniMet", 68 | "Major_cities": "Canadian cities", 69 | "MakeLayersVisible": "All layers are invisible, no animation/image can be created.", 70 | "MapCustomizations": "Customize map", 71 | "MissingTimesteps": "Failed to draw {TIMESTEP} for {LAYER}. Time removed from temporal extent.", 72 | "ModelRun": "Model run", 73 | "MP4CreateButtonLabel": "Create animation", 74 | "MP4CreateCancelAnimationCreation": "Cancel", 75 | "MP4CreateCustomTitle": "Animation title (optional)", 76 | "MP4CreateDarkMode": "Dark mode", 77 | "MP4CreateLightMode": "Light mode", 78 | "MP4CreateNotifyCancelAnimation": "The animation creation has been cancelled given your browser window has been resized", 79 | "MP4CreateTitle": "Animate", 80 | "MP4ExportDownload": "Download as MP4", 81 | "MP4ExportSubtitle": "Preview the created animation and download using the button below", 82 | "MP4ExportTitle": "Visualize & export", 83 | "MRNFTerritories": "QC MRNF - Territory", 84 | "MSCAnimet": "MSC AniMet", 85 | "MSCAnimetEmail": "Weather animation made with MSC AniMet", 86 | "NASA": "NASA - Earthdata", 87 | "NoBasemap": "Background color", 88 | "NoData": "No data available here", 89 | "NotFound": "404 Not Found", 90 | "NotFoundSub": "The page you are looking for does not exist.", 91 | "NoTimeTooltip": "Layer has no temporal dimension", 92 | "NRCan": "NRCan - CanVec", 93 | "OSM": "OpenStreetMap", 94 | "OtherProperties": "Properties", 95 | "OutputFormat": "Output format", 96 | "Overlay": "Overlays", 97 | "Places": "Places", 98 | "Portrait": "Portrait", 99 | "Presets": "Presets", 100 | "ResetRotation": "Reset rotation", 101 | "RevertColor": "Revert to original color", 102 | "RevertTimeZone": "Revert to your browser's time zone", 103 | "Reverse": "Play in reverse", 104 | "ReverseAnimation": "Animate in reverse", 105 | "SearchTZ": "Search time zone", 106 | "SelectCRS": "Select projection", 107 | "SelectMR": "Select model run", 108 | "ServiceRestored": "Service restored, resuming...", 109 | "Share": "Share", 110 | "ShowGraticules": "Show graticules", 111 | "Simplified": "Simplified boundaries", 112 | "SnapLayerToExtent": "Apply temporal extent and interval to animation.", 113 | "SnappedLayer": "Temporal extent and interval currently applied to animation.", 114 | "SocialWeatherHashtags": "weather,opendata", 115 | "Square": "Square", 116 | "Standard": "Standard", 117 | "StyleError": "The WMS request failed with the style included in the request. The layer's style has thus been reset to the default style and may not match with the displayed legend.", 118 | "TimestepsDropdown": "Interval", 119 | "TreeSearchLabel": "Search {wmsSource} layers", 120 | "UltraWideScreen": "Ultra-widescreen", 121 | "UnhandledError": "An unhandled error was encountered. The layer had to be removed.", 122 | "UserDoc": "User documentation", 123 | "Valid": "Valid time", 124 | "Value": "Value", 125 | "VideoFormat": "Resolution", 126 | "Water": "Water", 127 | "Water_bodies": "Canadian water bodies", 128 | "Weather": "GeoMet-Weather", 129 | "WeatherDev": "GeoMet-Weather Dev", 130 | "WeatherNightly": "GeoMet-Weather Nightly", 131 | "WeatherStage": "GeoMet-Weather Stage", 132 | "White": "White", 133 | "Widescreen": "Widescreen", 134 | "WmsSourceTitle": "Add {wmsSource} layers", 135 | "WrongTimeFormat": "The format used for time is not recognized by the application.\nThe WMS standard supports 4 ways of adding dates inside a WMS, which are:\n\tA single date;\n\tMultiple dates comma separated;\n\tStart/End/Interval;\n\tStart1/End1/Interval1,Start2/End2/Interval2,etc.\nIf you wish to add this layer to AniMet, you will need to update the WMS source accordingly." 136 | } 137 | -------------------------------------------------------------------------------- /src/locales/fr/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "AddArrow": "Flèche", 3 | "AddBox": "Rectangle", 4 | "AddCircle": "Cercle", 5 | "AddPolygon": "Polygone", 6 | "AddPreset": "Ajouter un préréglage", 7 | "AddTextbox": "Boite de texte", 8 | "AnimationFrom": "Animation météo de", 9 | "AppBarCreate": "Configuration", 10 | "AppBarExport": "Animation créée", 11 | "AppBarMap": "Carte", 12 | "ApplyColor": "Appliquer la couleur à la carte", 13 | "AspectSelection": "Sélectionner le format de sortie", 14 | "AttributionOSM": "Carte par © OpenStreetMap", 15 | "Base": "Originale", 16 | "Black": "Noire", 17 | "Boundaries": "Frontières", 18 | "BrokenLayer": "La couche ne peut présentement pas être chargée.", 19 | "Climate": "GeoMet-Climat", 20 | "ClimateDev": "GeoMet-Climat Dev", 21 | "ClimateNightly": "GeoMet-Climat Nightly", 22 | "ClimateStage": "GeoMet-Climat Stage", 23 | "Close": "Fermer", 24 | "CMD_WMS": "CMD-WMS", 25 | "Colon": " :", 26 | "ColorBorder": "Bordure des légendes en couleur", 27 | "ControllerOptions": "Options de lecture", 28 | "Dark": "Sombre", 29 | "DisplayLegend": "Afficher la légende", 30 | "DocumentationURL": "https://eccc-msc.github.io/open-data/msc-animet/readme_fr/", 31 | "ECMWF": "ECMWF - ecCharts", 32 | "ElementsOutOfFrame": "Les légendes et boites de texte doivent être placées dans le cadre pour apparaitre dans l'animation.", 33 | "EPSG3857": "Web Mercator", 34 | "EPSG3978": "Lambert Conique Conforme", 35 | "EPSG3995": "Arctique Polaire Stéréo", 36 | "EPSG4326": "WGS84 (lat/lon)", 37 | "ExpiredTimesteps": "Certains pas de temps ont expiré. Les informations temporelles ont été mises à jour.", 38 | "ExtentFullyOOB": "La plage complète des dates sélectionnées n'est plus disponible. La plage des dates sélectionnées a été réinitialisée.", 39 | "Fill": "Remplissage", 40 | "FPS": "IPS", 41 | "FramesPerSecond": "Images par seconde", 42 | "GoToHomePage": "Aller à la page d'accueil AniMet du SMC", 43 | "Grey": "Grise", 44 | "InvalidSRS": "Le SRC {CRS} n'est pas valide pour cette source de données.", 45 | "JPEGCreateButtonLabel": "Créer l'image", 46 | "JPEGExportDownload": "Télécharger au format JPEG", 47 | "JPEGExportSubtitle": "Prévisualiser l'image créée et télécharger à l'aide du bouton ci-dessous", 48 | "LayerBarClosestTime": "Date la plus proche: ", 49 | "LayerBarCurrentTooltip": "Actuel", 50 | "LayerBarEndsTooltip": "Fin", 51 | "LayerBarInvisibleTooltip": "Date la plus proche hors limites. Couche non-visible.", 52 | "LayerBarMapTime": "Date de la carte: ", 53 | "LayerBarMissingTimestep": "La date de la carte se situe entre deux pas de temps disponibles de la couche.", 54 | "LayerBarOpacity": "Opacité", 55 | "LayerBarRemoveTooltip": "Retirer la couche", 56 | "LayerBarStartsTooltip": "Début", 57 | "LayerBarStepTooltip": "Intervalle", 58 | "LayerBarVisibilityTooltip": "Régler la visibilité", 59 | "LayerControlsTitle": "Configurer", 60 | "LayerStyle": "Sélectionner le style de la couche", 61 | "LayerTree": "Ajouter", 62 | "Light": "Claire", 63 | "Loading": "Chargement...", 64 | "LocalTime": "Local", 65 | "Loop": "Lecture en boucle", 66 | "LoopRetry": "Service non disponible. Nouvelle tentative dans {SECONDS} secondes.", 67 | "MadeWithAniMet": "Créé avec AniMet du SMC", 68 | "Major_cities": "Villes canadiennes", 69 | "MakeLayersVisible": "Toutes les couches sont invisibles, aucune animation/image ne peut être créée.", 70 | "MapCustomizations": "Personnaliser la carte", 71 | "MissingTimesteps": "L'affichage du pas de temps {TIMESTEP} a échoué pour {LAYER}. Temps supprimé de l'étendue temporelle.", 72 | "ModelRun": "Passe du modèle", 73 | "MP4CreateButtonLabel": "Créer l'animation", 74 | "MP4CreateCancelAnimationCreation": "Annuler", 75 | "MP4CreateCustomTitle": "Titre de l'animation (optionnel)", 76 | "MP4CreateDarkMode": "Mode foncé", 77 | "MP4CreateLightMode": "Mode clair", 78 | "MP4CreateNotifyCancelAnimation": "La création de l'animation a été annulée car la fenêtre de votre navigateur a été redimensionnée.", 79 | "MP4CreateTitle": "Animer", 80 | "MP4ExportDownload": "Télécharger au format MP4", 81 | "MP4ExportSubtitle": "Prévisualiser l'animation créée et télécharger à l'aide du bouton ci-dessous", 82 | "MP4ExportTitle": "Visualiser et exporter", 83 | "MSCAnimet": "AniMet du SMC", 84 | "MSCAnimetEmail": "Animation météo créée à partir d'AniMet du SMC", 85 | "NASA": "NASA - Earthdata", 86 | "NoBasemap": "Couleur de fond", 87 | "NoData": "Aucune donnée disponible ici", 88 | "NotFound": "Erreur 404", 89 | "NotFoundSub": "La page que vous recherchez est introuvable.", 90 | "NoTimeTooltip": "La couche ne possède pas de dimension temporelle", 91 | "NRCan": "NRCan - CanVec", 92 | "OSM": "OpenStreetMap", 93 | "OtherProperties": "Propriétés", 94 | "OutputFormat": "Format de sortie", 95 | "Overlay": "Superpositions", 96 | "Places": "Lieux", 97 | "Portrait": "Portrait", 98 | "Presets": "Préréglages", 99 | "ResetRotation": "Réinitialiser la rotation", 100 | "RevertColor": "Remettre la couleur originale", 101 | "RevertTimeZone": "Remettre le fuseau de votre navigateur", 102 | "Reverse": "Lecture à reculons", 103 | "ReverseAnimation": "Animer à reculons", 104 | "SearchTZ": "Rechercher fuseau horaire", 105 | "SelectCRS": "Sélectionner la projection", 106 | "SelectMR": "Sélectionner la passe de modèle", 107 | "MRNFTerritories": "QC MRNF - Territoire", 108 | "ServiceRestored": "Service rétabli, reprise...", 109 | "Share": "Partager", 110 | "ShowGraticules": "Montrer les graticules", 111 | "Simplified": "Frontières simplifiées", 112 | "SnapLayerToExtent": "Appliquer l'étendue temporelle et l'intervalle à l'animation.", 113 | "SnappedLayer": "Étendue temporelle et intervalle appliqués à l'animation.", 114 | "SocialWeatherHashtags": "meteo,donneesouvertes", 115 | "Square": "Carré", 116 | "Standard": "Standard", 117 | "StyleError": "La requête WMS n'a pas fonctionné en incluant le style dans la requête. Le style de la couche a donc été réinitialisé au style par défaut et peut ne pas correspondre à la légende affichée.", 118 | "TimestepsDropdown": "Pas de temps", 119 | "TreeSearchLabel": "Chercher les couches {wmsSource}", 120 | "UltraWideScreen": "Ultra-large", 121 | "UnhandledError": "Une erreur non répertoriée a été rencontrée. La couche a dû être retirée.", 122 | "UserDoc": "Documentation usager", 123 | "Valid": "Heure valide", 124 | "Value": "Valeur", 125 | "VideoFormat": "Résolution", 126 | "Water": "Eau", 127 | "Water_bodies": "Délimitations des plans d'eau canadiens", 128 | "Weather": "GeoMet-Météo", 129 | "WeatherDev": "GeoMet-Météo Dev", 130 | "WeatherNightly": "GeoMet-Météo Nightly", 131 | "WeatherStage": "GeoMet-Météo Stage", 132 | "White": "Blanche", 133 | "Widescreen": "Large", 134 | "WmsSourceTitle": "Ajouter des couches {wmsSource}", 135 | "WrongTimeFormat": "Le format utilisé pour le temps n'est pas reconnu par l'application.\nLe standard WMS supporte 4 façons d'ajouter vos dates à l'intérieur d'un WMS, soit:\n\tUne seule date;\n\tPlusieurs dates séparées d'une virgule;\n\tDébut/Fin/Intervalle;\n\tDébut1/Fin1/Intervalle1,Début2/Fin2/Intervalle2,etc.\nSi vous souhaitez ajouter cette couche dans AniMet, veuillez faire les changements appropriés dans votre source WMS." 136 | } 137 | -------------------------------------------------------------------------------- /src/locales/importLocaleFiles.js: -------------------------------------------------------------------------------- 1 | // src/locales/importLocale.js 2 | const importEnLocaleFiles = import.meta.glob('./en/layers_*.json', {eager: true}); 3 | const importFrLocaleFiles = import.meta.glob('./fr/layers_*.json', {eager: true}); 4 | 5 | let enLocaleData = {}; 6 | let frLocaleData = {}; 7 | 8 | // Process English locales 9 | for (const path in importEnLocaleFiles) { 10 | const sourceName = path.match(/layers_(.+)\.json$/)[1]; 11 | enLocaleData[sourceName] = importEnLocaleFiles[path].default; 12 | } 13 | 14 | // Process French locales 15 | for (const path in importFrLocaleFiles) { 16 | const sourceName = path.match(/layers_(.+)\.json$/)[1]; 17 | frLocaleData[sourceName] = importFrLocaleFiles[path].default; 18 | } 19 | 20 | const localeData = { enLocaleData, frLocaleData }; 21 | 22 | export default localeData; 23 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { registerPlugins } from '@/plugins' 2 | import { useStore } from '@/stores/store' 3 | 4 | import mitt from 'mitt' 5 | import * as ct from 'countries-and-timezones' 6 | 7 | import App from './App.vue' 8 | 9 | import { createApp, reactive } from 'vue' 10 | 11 | const emitter = mitt() 12 | const app = createApp(App) 13 | 14 | registerPlugins(app) 15 | app.provide('store', useStore()) 16 | 17 | app.config.globalProperties.$mapLayers = reactive({ arr: [] }) 18 | app.config.globalProperties.$mapCanvas = reactive({ mapObj: {} }) 19 | app.config.globalProperties.$animationCanvas = reactive({ mapObj: {} }) 20 | 21 | app.config.globalProperties.emitter = emitter 22 | 23 | const originalConsoleError = console.error 24 | function customLog(message) { 25 | // OpenLayers added an annoying console.error everytime a WMS request 26 | // returns XML even if it's handled so this code is there to silence it 27 | if (!(message instanceof DOMException)) originalConsoleError(message) 28 | } 29 | console.error = customLog 30 | 31 | const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone 32 | const country = ct.getCountryForTimezone(timeZone) 33 | 34 | app.config.globalProperties.$timeZone = reactive({ id: timeZone }) 35 | if (country === null) { 36 | app.config.globalProperties.$countryCode = reactive({ id: null }) 37 | } else { 38 | app.config.globalProperties.$countryCode = reactive({ id: country.id }) 39 | } 40 | app.config.globalProperties.$ct = ct 41 | 42 | app.mount('#app') 43 | -------------------------------------------------------------------------------- /src/plugins/i18n.js: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import localeData from '../locales/importLocaleFiles' 3 | 4 | const enCommonPath = '../locales/en/common.json' 5 | let en = { 6 | ...import.meta.glob('../locales/en/common.json', { eager: true })[ 7 | enCommonPath 8 | ], 9 | } 10 | for (const src in localeData['enLocaleData']) { 11 | Object.assign(en, localeData['enLocaleData'][src]) 12 | } 13 | 14 | const frCommonPath = '../locales/fr/common.json' 15 | let fr = { 16 | ...import.meta.glob('../locales/fr/common.json', { eager: true })[ 17 | frCommonPath 18 | ], 19 | } 20 | for (const src in localeData['frLocaleData']) { 21 | Object.assign(fr, localeData['frLocaleData'][src]) 22 | } 23 | 24 | const messages = { 25 | en: en, 26 | fr: fr, 27 | } 28 | 29 | let locale = navigator.language.split('-')[0] 30 | 31 | export default createI18n({ 32 | legacy: false, 33 | locale: locale, 34 | fallbackLocale: 'en', 35 | messages, 36 | silentFallbackWarn: true, 37 | }) 38 | -------------------------------------------------------------------------------- /src/plugins/index.js: -------------------------------------------------------------------------------- 1 | import i18n from './i18n' 2 | import vuetify from './vuetify' 3 | import pinia from '@/stores' 4 | import router from '@/router' 5 | 6 | export function registerPlugins (app) { 7 | app 8 | .use(i18n) 9 | .use(vuetify) 10 | .use(router) 11 | .use(pinia) 12 | } 13 | -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import '@mdi/font/css/materialdesignicons.css' 2 | import 'vuetify/styles' 3 | 4 | import { createVuetify } from 'vuetify' 5 | 6 | export default createVuetify({ 7 | theme: { 8 | defaultTheme: 'dark', 9 | themes: { 10 | dark: { 11 | colors: { 12 | snackbarBackground: '#424242', 13 | snackbarText: '#eeeeee', 14 | primary: '#1689E7', 15 | accent: '#4CBB99', 16 | secondary: '#7BC6FF', 17 | success: '#4CAF50', 18 | info: '#2196F3', 19 | warning: '#FB8C00', 20 | error: '#FF5252', 21 | }, 22 | }, 23 | light: { 24 | colors: { 25 | primary: '#1689E7', 26 | accent: '#4CBB99', 27 | secondary: '#7BC6FF', 28 | success: '#4CAF50', 29 | info: '#2196F3', 30 | warning: '#FB8C00', 31 | error: '#FF5252', 32 | }, 33 | }, 34 | }, 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router/auto' 2 | 3 | import Home from '@/views/Home.vue' 4 | import MultiDisplay from '@/views/MultiDisplay.vue' 5 | import NotFound from '@/views/NotFound.vue' 6 | 7 | const routes = [ 8 | { 9 | path: '/', 10 | name: 'home', 11 | component: Home, 12 | props: (route) => ({ 13 | layers: route.query.layers, 14 | extent: route.query.extent, 15 | color: route.query.color, 16 | basemap: route.query.basemap, 17 | overlays: route.query.overlays, 18 | proj: route.query.proj, 19 | grat: route.query.grat, 20 | play: route.query.play, 21 | }), 22 | }, 23 | { 24 | path: '/:number(1|2|3|4)-displays', 25 | name: 'MultiDisplay', 26 | component: MultiDisplay, 27 | props: (route) => ({ 28 | disp: route.query.disp, 29 | }), 30 | }, 31 | { 32 | path: '/:pathMatch(.*)*', 33 | name: 'NotFound', 34 | component: NotFound, 35 | }, 36 | ] 37 | 38 | const router = createRouter({ 39 | history: createWebHistory(import.meta.env.BASE_URL), 40 | routes: routes, 41 | }) 42 | 43 | router.beforeEach((to, from, next) => { 44 | // Normalize the path by removing multiple slashes 45 | const cleanedPath = to.path.replace(/\/\/+/g, '/') 46 | const cleanedFullPath = to.fullPath.replace(/\/\/+/g, '/') 47 | let newPath 48 | if (cleanedPath === '/') { 49 | newPath = cleanedFullPath 50 | } else { 51 | newPath = cleanedFullPath.replace(cleanedPath, '') 52 | } 53 | 54 | if (cleanedPath !== to.path) { 55 | next({ path: newPath, replace: true }) 56 | } else { 57 | next() 58 | } 59 | }) 60 | 61 | // Workaround for https://github.com/vitejs/vite/issues/11804 62 | router.onError((err, to) => { 63 | if (err?.message?.includes?.('Failed to fetch dynamically imported module')) { 64 | if (!localStorage.getItem('vuetify:dynamic-reload')) { 65 | console.log('Reloading page to fix dynamic import error') 66 | localStorage.setItem('vuetify:dynamic-reload', 'true') 67 | location.assign(to.fullPath) 68 | } else { 69 | console.error('Dynamic import error, reloading page did not fix it', err) 70 | } 71 | } else { 72 | console.error(err) 73 | } 74 | }) 75 | 76 | router.isReady().then(() => { 77 | localStorage.removeItem('vuetify:dynamic-reload') 78 | }) 79 | 80 | export default router 81 | -------------------------------------------------------------------------------- /src/stores/index.js: -------------------------------------------------------------------------------- 1 | // Utilities 2 | import { createPinia } from 'pinia' 3 | 4 | export default createPinia() 5 | -------------------------------------------------------------------------------- /src/utils/AxiosConfig.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import axiosRetry from "axios-retry"; 3 | 4 | axiosRetry(axios, { 5 | retries: 4, 6 | retryDelay: (retryCount) => { 7 | return retryCount * 800; 8 | }, 9 | }); 10 | 11 | export default axios; 12 | -------------------------------------------------------------------------------- /src/utils/IntegerAssigner.js: -------------------------------------------------------------------------------- 1 | export default class IntegerAssigner { 2 | constructor() { 3 | this.nextInteger = 0; 4 | this.mappedIntegers = new Map(); 5 | this.freedIntegers = []; 6 | } 7 | 8 | addItem(item) { 9 | if (this.mappedIntegers.has(item)) { 10 | return; 11 | } 12 | 13 | if (this.freedIntegers.length === 0) { 14 | this.mappedIntegers.set(item, this.nextInteger); 15 | this.nextInteger++; 16 | } else { 17 | const freedInteger = this.freedIntegers.shift(); 18 | this.mappedIntegers.set(item, freedInteger); 19 | } 20 | } 21 | 22 | removeItem(item) { 23 | if (!this.mappedIntegers.has(item)) { 24 | return; 25 | } 26 | const integer = this.mappedIntegers.get(item); 27 | this.mappedIntegers.delete(item); 28 | this.freedIntegers.push(integer); 29 | this.freedIntegers.sort(function (a, b) { 30 | return a - b; 31 | }); 32 | } 33 | 34 | getItemInteger(item) { 35 | return this.mappedIntegers.get(item); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 178 | 179 | 185 | -------------------------------------------------------------------------------- /src/views/MultiDisplay.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | 183 | 184 | 191 | -------------------------------------------------------------------------------- /src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ $t('NotFound') }} 5 | 6 | {{ $t('NotFoundSub') }} 7 | 8 | 9 | 10 | {{ $t('GoToHomePage') }} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 33 | -------------------------------------------------------------------------------- /vite.config.mjs: -------------------------------------------------------------------------------- 1 | // Plugins 2 | import AutoImport from 'unplugin-auto-import/vite' 3 | import Components from 'unplugin-vue-components/vite' 4 | import Layouts from 'vite-plugin-vue-layouts' 5 | import { nodePolyfills } from 'vite-plugin-node-polyfills' 6 | import Vue from '@vitejs/plugin-vue' 7 | import VueRouter from 'unplugin-vue-router/vite' 8 | import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' 9 | 10 | // Utilities 11 | import { defineConfig, loadEnv } from 'vite' 12 | import { fileURLToPath, URL } from 'node:url' 13 | 14 | // https://vitejs.dev/config/ 15 | export default ({ mode }) => { 16 | process.env = { ...process.env, ...loadEnv(mode, process.cwd()) } 17 | 18 | return defineConfig({ 19 | base: process.env.VITE_BASE_URL, 20 | transpileDependencies: true, 21 | plugins: [ 22 | nodePolyfills(), 23 | VueRouter(), 24 | Layouts(), 25 | Vue({ 26 | template: { transformAssetUrls }, 27 | }), 28 | // https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme 29 | Vuetify({ 30 | autoImport: true, 31 | }), 32 | Components(), 33 | AutoImport({ 34 | imports: ['vue', 'vue-router'], 35 | eslintrc: { 36 | enabled: true, 37 | }, 38 | vueTemplate: true, 39 | }), 40 | ], 41 | resolve: { 42 | alias: { 43 | '@': fileURLToPath(new URL('./src', import.meta.url)), 44 | }, 45 | extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue'], 46 | }, 47 | server: { 48 | port: 3000, 49 | }, 50 | }) 51 | } 52 | --------------------------------------------------------------------------------
6 | {{ $t('NotFoundSub') }} 7 |
9 | 10 | {{ $t('GoToHomePage') }} 11 | 12 |