├── .dockerignore ├── .env ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc.json ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── scripts └── parse-env.sh ├── src ├── config.js ├── index.css ├── index.js ├── scenes │ └── papers │ │ ├── Papers.css │ │ ├── Papers.js │ │ ├── components │ │ ├── Header.css │ │ ├── Header.js │ │ ├── Panel.css │ │ ├── Panel.js │ │ ├── Sidebar.js │ │ ├── header │ │ │ ├── HeaderModal.css │ │ │ └── HeaderModal.js │ │ └── panel │ │ │ ├── map │ │ │ ├── Map.css │ │ │ ├── Map.js │ │ │ ├── MapInfoBox.js │ │ │ ├── MapLayerControl.js │ │ │ ├── MapSelectPaper.js │ │ │ ├── MapTileLayer.js │ │ │ └── events │ │ │ │ └── MapClickEvent.js │ │ │ └── search │ │ │ ├── JargonSearch.js │ │ │ ├── PaperSearch.js │ │ │ └── Search.css │ │ ├── controllers │ │ ├── backend │ │ │ ├── JargonSearch.js │ │ │ └── MetricSearch.js │ │ └── paperscape │ │ │ ├── MapConfig.js │ │ │ ├── MapLabels.js │ │ │ ├── PaperInfo.js │ │ │ ├── PaperPosition.js │ │ │ ├── PaperSearch.js │ │ │ └── PaperSearchPosition.js │ │ ├── images │ │ └── DS3_logo.png │ │ └── models │ │ ├── PaperInfo.js │ │ ├── PaperMetric.js │ │ └── PaperPosition.js └── serviceWorker.js └── tests └── papers ├── Papers.test.js └── components ├── Header.test.js ├── Panel.test.js ├── Sidebar.test.js ├── header └── HeaderModal.test.js └── panel └── search ├── JargonSearch.test.js └── PaperSearch.test.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # Hidden items 2 | .dockerignore 3 | .gitignore 4 | .git 5 | 6 | # Artifacts that will be built during image creation 7 | build 8 | node_modules 9 | 10 | # Development mode files 11 | public/*.js 12 | tests 13 | 14 | # Ignore any markdown document 15 | **/*.md 16 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | SERVER_API_HOST=http://0.0.0.0 2 | SERVER_API_PORT=8080 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | 5 | - package-ecosystem: github-actions 6 | directory: "/" 7 | schedule: 8 | interval: weekly 9 | ignore: 10 | # Ignore auto-updates on SemVer major releases 11 | - dependency-name: "*" 12 | update-types: ["version-update:semver-major"] 13 | 14 | - package-ecosystem: npm 15 | directory: "/" 16 | schedule: 17 | interval: weekly 18 | ignore: 19 | # Ignore auto-updates on SemVer major releases 20 | - dependency-name: "*" 21 | update-types: ["version-update:semver-major"] 22 | allow: 23 | - dependency-type: development 24 | - dependency-type: production 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | paths-ignore: 9 | - "**.md" 10 | pull_request: 11 | branches: 12 | - main 13 | paths-ignore: 14 | - "**.md" 15 | 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | 22 | jobs: 23 | 24 | lint: 25 | needs: [] 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: "Set up GitHub Actions" 29 | uses: actions/checkout@v4 30 | - name: "Set up Node" 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: 20 34 | - name: "Install Prettier" 35 | run: npm install --global prettier@3.0.1 36 | - name: "Check format" 37 | run: make check 38 | 39 | test: 40 | needs: [lint] 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: "Set up GitHub Actions" 44 | uses: actions/checkout@v4 45 | - name: "Set up Node" 46 | uses: actions/setup-node@v4 47 | with: 48 | node-version: 20 49 | - name: "Install dependencies" 50 | run: npm install-clean 51 | - name: "Run tests" 52 | run: make test 53 | 54 | build: 55 | needs: [test] 56 | runs-on: ubuntu-latest 57 | steps: 58 | - name: "Set up GitHub Actions" 59 | uses: actions/checkout@v4 60 | - name: "Build Docker image" 61 | run: make docker-build 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS and IDE aux files 2 | **/.DS_Store 3 | .idea 4 | 5 | # Dependencies 6 | node_modules 7 | 8 | # Testing 9 | coverage 10 | 11 | # Production 12 | build 13 | 14 | # Environment files 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | # Development mode env. values file 21 | public/*.js 22 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "insertPragma": false, 5 | "printWidth": 90, 6 | "tabWidth": 4, 7 | "trailingComma": "es5" 8 | } 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image 2 | FROM node:20.10-alpine 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | 8 | # Copy dependency files 9 | COPY package.json /app 10 | COPY package-lock.json /app 11 | 12 | # Install dependencies 13 | RUN npm install-clean --omit=dev && \ 14 | npm install --global serve && \ 15 | npm cache clean --force 16 | 17 | # Copy all files 18 | COPY . /app 19 | 20 | 21 | # Build for production 22 | RUN npm run build --silent 23 | 24 | 25 | # State application exposed port 26 | EXPOSE 5000 27 | 28 | # Start app 29 | CMD ["/bin/sh", "-c", "./scripts/parse-env.sh && serve --listen tcp://0.0.0.0:5000 --single 'build'"] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 New York University 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP_VERSION = $(shell cat VERSION) 2 | IMAGE_NAME = "dialect-map-ui" 3 | SOURCE_FOLDER = "src" 4 | TESTS_FOLDER = "tests" 5 | 6 | GCP_PROJECT ?= "ds3-dialect-map" 7 | GCP_REGISTRY ?= "us.gcr.io" 8 | GCP_IMAGE_NAME = $(GCP_REGISTRY)/$(GCP_PROJECT)/$(IMAGE_NAME) 9 | 10 | 11 | .PHONY: check 12 | check: 13 | @echo "Checking code format" 14 | @npx prettier --check "$(SOURCE_FOLDER)/**/*.js" 15 | @npx prettier --check "$(TESTS_FOLDER)/**/*.js" 16 | 17 | 18 | .PHONY: build 19 | build: 20 | @echo "Building project" 21 | @npm run build --silent 22 | 23 | 24 | .PHONY: run 25 | run: 26 | @echo "Running development project" 27 | @npm run start --silent 28 | 29 | 30 | .PHONY: test 31 | test: 32 | @echo "Testing project" 33 | @npx jest --noStackTrace $(TESTS_FOLDER) 34 | 35 | 36 | .PHONY: docker-build 37 | docker-build: 38 | @echo "Building Docker image" 39 | @docker build . --tag $(IMAGE_NAME):$(APP_VERSION) 40 | 41 | 42 | .PHONY: docker-push 43 | docker-push: docker-build 44 | @echo "Pushing Docker image to GCP" 45 | @docker tag $(IMAGE_NAME):$(APP_VERSION) $(GCP_IMAGE_NAME):$(APP_VERSION) 46 | @docker push $(GCP_IMAGE_NAME):$(APP_VERSION) 47 | @docker rmi $(GCP_IMAGE_NAME):$(APP_VERSION) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dialect map UI 2 | 3 | [![CI/CD Status][ci-status-badge]][ci-status-link] 4 | [![Coverage Status][cov-status-badge]][cov-status-link] 5 | [![MIT license][mit-license-badge]][mit-license-link] 6 | [![Code style][code-style-badge]][code-style-link] 7 | 8 | This repository contains the front-end interface of the Dialect map project. 9 | An idea proposed by professor Kyle Cranmer, and initially developed as one 10 | of the _Data Science and Software Services_ (DS3) funded projects. 11 | 12 | 13 | ## About 14 | This project provides a web interface for the [Dialect Map project][dialect-map-repo], 15 | taking the interface of the [PaperScape project][paperscape-blog] as inspiration, 16 | but modernizing and standardizing the whole stack. This web client uses: 17 | 18 | - [React][webpage-react] ⚛️ 19 | - [Leaflet][webpage-leaflet] 🗺️ 20 | - [Semantic][webpage-semantic] 🎨 21 | 22 | 23 | ## Environment setup 24 | All dependencies can be installed by running: 25 | 26 | ```shell 27 | npm install 28 | npm install --global serve 29 | ``` 30 | 31 | 32 | ### Formatting 33 | All JavaScript files are formatted using [Prettier][webpage-prettier], and the custom properties 34 | defined in the `.prettierrc.json` file. To check for code style inconsistencies: 35 | 36 | ```shell 37 | make check 38 | ``` 39 | 40 | ### Testing 41 | Project testing is performed using [Jest][webpage-jest]. In order to run the tests: 42 | 43 | ```shell 44 | make test 45 | ``` 46 | 47 | 48 | ### Run project 49 | To start a development server: 50 | 51 | ```shell 52 | make run 53 | ``` 54 | 55 | 56 | ## Docker 57 | The project is currently designed to be deployed in a Google Cloud Platform project, 58 | so the initial step involves using [gcloud][docs-gcloud-cli] CLI tool to log into it: 59 | 60 | ```shell 61 | gcloud login 62 | gcloud auth configure-docker 63 | ``` 64 | 65 | To build a Docker image out of the project: 66 | 67 | ```shell 68 | make docker-build 69 | ``` 70 | 71 | To push a Docker image to the GCP registry: 72 | 73 | ```shell 74 | export GCP_PROJECT="ds3-dialect-map" 75 | export GCP_REGISTRY="us.gcr.io" 76 | make docker-push 77 | ``` 78 | 79 | 80 | ## Deployment 81 | This project uses a set of env. variables to configure the connection with the backend API: 82 | 83 | Bear in mind that React does not allow passing **run-time** env. variables to a built application 84 | ([reference][docs-react-env]). In order to do so, a dedicated shell script named `parse-env.sh` was created. 85 | This script parses the `.env` file substituting the default values by the ones defined in the environment, 86 | before writing them into a _run-time generated_ JavaScript file (loaded from the `index.html`). 87 | 88 | | ENV VARIABLE | DEFAULT | REQUIRED | DESCRIPTION | 89 | |--------------------------|--------------------|----------|-----------------------------------------------| 90 | | SERVER_API_HOST | http://0.0.0.0 | No | Backend API host to connect | 91 | | SERVER_API_PORT | 8080 | No | Backend API port to connect | 92 | 93 | 94 | ## Acknowledges 95 | 96 | We would like to thank the PaperScape authors for maintaining an up-to-date tiles server, 97 | and specially Rob J. Knegjens for being in contact with us during the development of this project. 98 | 99 | 100 | [ci-status-badge]: https://github.com/dialect-map/dialect-map-ui/actions/workflows/ci.yml/badge.svg?branch=main 101 | [ci-status-link]: https://github.com/dialect-map/dialect-map-ui/actions/workflows/ci.yml?query=branch%3Amain 102 | [code-style-badge]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg 103 | [code-style-link]: https://github.com/prettier/prettier 104 | [cov-status-badge]: https://codecov.io/gh/dialect-map/dialect-map-ui/branch/main/graph/badge.svg 105 | [cov-status-link]: https://codecov.io/gh/dialect-map/dialect-map-ui 106 | [mit-license-badge]: https://img.shields.io/badge/License-MIT-blue.svg 107 | [mit-license-link]: https://github.com/dialect-map/dialect-map-ui/blob/main/LICENSE 108 | 109 | [dialect-map-repo]: https://github.com/dialect-map/dialect-map 110 | [docs-gcloud-cli]: https://cloud.google.com/sdk/docs/install 111 | [docs-react-env]: https://create-react-app.dev/docs/adding-custom-environment-variables/ 112 | [paperscape-blog]: https://paperscape.org/ 113 | [webpage-jest]: https://jestjs.io/ 114 | [webpage-leaflet]: https://leafletjs.com/ 115 | [webpage-prettier]: https://prettier.io/docs/en/index.html 116 | [webpage-react]: https://reactjs.org/ 117 | [webpage-semantic]: https://react.semantic-ui.com/ 118 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.3.0 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dialect-map-ui", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "private": true, 6 | "dependencies": { 7 | "leaflet": "^1.9.3", 8 | "react": "^17.0.1", 9 | "react-dom": "^17.0.1", 10 | "react-leaflet": "^3.2.5", 11 | "react-router-dom": "^6.2.1", 12 | "react-scripts": "^5.0.1", 13 | "semantic-ui-css": "^2.5.0", 14 | "semantic-ui-react": "^2.1.4" 15 | }, 16 | "devDependencies": { 17 | "@babel/preset-env": "^7.16.0", 18 | "@babel/preset-react": "^7.16.0", 19 | "@testing-library/react": "^11.2.7", 20 | "jest": "^27.5.1", 21 | "prettier": "3.0.1", 22 | "react-test-renderer": "^17.0.2" 23 | }, 24 | "scripts": { 25 | "prestart": "./scripts/parse-env.sh --destination public", 26 | "start": "react-scripts start", 27 | "build": "react-scripts build" 28 | }, 29 | "babel": { 30 | "presets": [ 31 | ["@babel/preset-env", {"modules": "auto"}], 32 | ["@babel/preset-react", {"runtime": "automatic"}] 33 | ], 34 | "plugins": [ 35 | "@babel/plugin-transform-runtime" 36 | ] 37 | }, 38 | "jest": { 39 | "testEnvironment": "jsdom", 40 | "moduleNameMapper": { 41 | "\\.(jpg|jpeg|png|gif|svg)$": "babel-jest", 42 | "\\.(css|less)$": "babel-jest" 43 | } 44 | }, 45 | "browserslist": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialect-map/dialect-map-ui/0791322bed1f4e57c64de4561cc30b0ca4d0440e/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | 17 | 21 | 22 | 26 | 27 | Dialect map 28 | 29 | 30 | 31 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Dialect map", 3 | "name": "Dialect map UI", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /scripts/parse-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### NOTE: 4 | ### 5 | ### Script that parses every variable defined in the source file 6 | ### in order to replace the default values with environment values, 7 | ### and dump them into a JavaScript file that gets loaded by the app. 8 | ### 9 | ### Arguments: 10 | ### --destination: Destination folder within the project root (i.e. public). 11 | ### --output_file: Name of the run-time generated output JavaScript file. 12 | ### --source_file: Name of the source file to read variables names from. 13 | 14 | # Define default values 15 | destination="build" 16 | output_file="environment.js" 17 | source_file=".env" 18 | 19 | # Argument parsing 20 | while [ "$#" -gt 0 ]; do 21 | case $1 in 22 | -d|--destination) destination=${2}; shift ;; 23 | -o|--output_file) output_file=${2}; shift ;; 24 | -s|--source_file) source_file=${2}; shift ;; 25 | *) echo "Unknown parameter passed: $1"; exit 1 ;; 26 | esac 27 | shift 28 | done 29 | 30 | 31 | PROJECT_DIR="$(dirname "$0")/.." 32 | 33 | # Define file paths 34 | OUTPUT_PATH="${PROJECT_DIR}/${destination}/${output_file}" 35 | SOURCE_PATH="${PROJECT_DIR}/${source_file}" 36 | 37 | # Define AWK expressions to parse file and get env. vars 38 | AWK_PAD_EXP="\" \"" 39 | AWK_KEY_EXP="\$1" 40 | AWK_VAL_EXP="(ENVIRON[\$1] ? ENVIRON[\$1] : \$2)" 41 | AWK_ALL_EXP="{ print ${AWK_PAD_EXP} ${AWK_KEY_EXP} \": '\" ${AWK_VAL_EXP} \"',\" }" 42 | 43 | 44 | # Build the run-time generated JavaScript environment file 45 | echo "window.env = {" > "${OUTPUT_PATH}" 46 | awk -F "=" "${AWK_ALL_EXP}" "${SOURCE_PATH}" >> "${OUTPUT_PATH}" 47 | echo "}" >> "${OUTPUT_PATH}" 48 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | /* NOTE: 4 | * Previously, a CORS-proxy was necessary to parse PaperScape responses 5 | * As they may not include the 'Access-Control-Allow-Origin' header. 6 | * Consider these resources if this change in the future: 7 | * 8 | * Ref: https://stackoverflow.com/questions/43262121/trying-to-use-fetch-and-pass-in-mode-no-cors 9 | * Redirect: https://cors-anywhere.herokuapp.com/ 10 | */ 11 | 12 | /* NOTE: 13 | * 14 | * Define browser environment fall back in order 15 | * to allow browser independent rendering (testing) 16 | */ 17 | const env = window.env || {}; 18 | 19 | const config = { 20 | /* Leaflet map properties */ 21 | mapBoundsCoords: [ 22 | [-2000, 0], 23 | [0, 2000], 24 | ], 25 | mapBoundsViscosity: 0.8, 26 | mapInitialCenter: [-1000, 1000], 27 | mapInitialZoom: 0, 28 | mapZoomMinimum: 0, 29 | mapZoomMaximum: 6, 30 | mapZoomControl: false, 31 | mapZoomDelta: 0.25, 32 | mapZoomSnap: 0.25, 33 | mapSubdomains: ["1", "2", "3", "4"], 34 | 35 | /* PaperScape map properties 36 | * 37 | * They change in a daily basis. 38 | * They need to be fetched prior any rendering 39 | */ 40 | worldMinX: null, 41 | worldMaxX: null, 42 | worldMinY: null, 43 | worldMaxY: null, 44 | worldTileSize: null, 45 | 46 | viewToWorldScale: null, 47 | worldToViewScale: null, 48 | 49 | worldLabels: { 50 | 0: { nx: 1, ny: 1 }, 51 | 1: { nx: 1, ny: 1 }, 52 | 2: { nx: 1, ny: 1 }, 53 | 3: { nx: 1, ny: 1 }, 54 | 4: { nx: 2, ny: 2 }, 55 | 5: { nx: 4, ny: 4 }, 56 | 6: { nx: 8, ny: 8 }, 57 | }, 58 | 59 | /* Dialect map server */ 60 | dialectMapHost: env.SERVER_API_HOST, 61 | dialectMapPort: env.SERVER_API_PORT, 62 | dialectMapURL: `${env.SERVER_API_HOST}:${env.SERVER_API_PORT}`, 63 | 64 | /* PaperScape URLs */ 65 | papersDataURL: "https://paperscape.org/wombat", 66 | worldConfigURL: "https://tile1.paperscape.org/world/world_index.json", 67 | labelsJsonHost: "https://tile1.paperscape.org/world/zones", 68 | 69 | /* PaperScape tiles URLs */ 70 | tilesColorHost: "https://tile{s}.paperscape.org/world/tiles/{z}/{x}/{y}.png", 71 | tilesGreyHost: "https://tile{s}.paperscape.org/world/tiles-hm/{z}/{x}/{y}.png", 72 | tilesAttrib: "PaperScape", 73 | }; 74 | 75 | export default config; 76 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 6 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | #root { 12 | height: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import ReactDOM from "react-dom"; 4 | import * as serviceWorker from "./serviceWorker"; 5 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 6 | import PapersMap from "./scenes/papers/Papers"; 7 | import "semantic-ui-css/semantic.min.css"; 8 | import "./index.css"; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | } /> 14 | } /> 15 | 16 | , 17 | document.getElementById("root") 18 | ); 19 | 20 | // If you want your app to work offline and load faster, you can change 21 | // unregister() to register() below. Note this comes with some pitfalls. 22 | // Learn more about service workers: http://bit.ly/CRA-PWA 23 | serviceWorker.register(); 24 | -------------------------------------------------------------------------------- /src/scenes/papers/Papers.css: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | .papers-layout { 4 | height: 100%; 5 | } 6 | 7 | .papers-header { 8 | min-height: 50px; 9 | min-width: 950px; 10 | padding-bottom: 0 !important; /* Necessary flag */ 11 | } 12 | 13 | .papers-body { 14 | height: calc(100% - 60px); 15 | min-width: 950px; 16 | } 17 | -------------------------------------------------------------------------------- /src/scenes/papers/Papers.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { Component } from "react"; 4 | import { Grid } from "semantic-ui-react"; 5 | import MapConfigCtl from "./controllers/paperscape/MapConfig"; 6 | import PapersHeader from "./components/Header"; 7 | import PapersPanel from "./components/Panel"; 8 | import "./Papers.css"; 9 | 10 | export default class PapersMap extends Component { 11 | /** Component containing the whole papers map scene */ 12 | 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | configLoaded: false, 17 | }; 18 | 19 | // Necessary binding to use this function in the React lifecycle 20 | this.setConfigLoaded = this.setConfigLoaded.bind(this); 21 | } 22 | 23 | componentDidMount() { 24 | MapConfigCtl.fetchConfig().then(this.setConfigLoaded); 25 | } 26 | 27 | setConfigLoaded() { 28 | this.setState({ configLoaded: true }); 29 | } 30 | 31 | render() { 32 | if (this.state.configLoaded === false) { 33 | return null; 34 | } 35 | 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/scenes/papers/components/Header.css: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | .papers-header-segment { 4 | border-radius: 0 !important; /* Necessary flag */ 5 | padding-top: 5px !important; /* Necessary flag */ 6 | padding-bottom: 5px !important; /* Necessary flag */ 7 | } 8 | -------------------------------------------------------------------------------- /src/scenes/papers/components/Header.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { Component } from "react"; 4 | import { Header, Menu, Segment } from "semantic-ui-react"; 5 | import HeaderModal from "./header/HeaderModal"; 6 | import "./Header.css"; 7 | 8 | export default class PapersHeader extends Component { 9 | /** Component defining the global scene header */ 10 | 11 | render() { 12 | return ( 13 | 14 | 15 | 16 |
Dialect Map
17 |
18 | 19 | 20 | 21 | 22 |
23 |
24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/scenes/papers/components/Panel.css: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | .panel-body-sidebar { 4 | min-width: 100px; 5 | padding-bottom: 0 !important; /* Necessary flag */ 6 | } 7 | 8 | .panel-body-main { 9 | max-width: calc(100% - 100px); 10 | padding-bottom: 0 !important; /* Necessary flag */ 11 | } 12 | 13 | .panel-body-header { 14 | margin-bottom: 10px; 15 | } 16 | 17 | .panel-body-map { 18 | height: 100%; 19 | position: relative; 20 | } 21 | -------------------------------------------------------------------------------- /src/scenes/papers/components/Panel.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { Component } from "react"; 4 | import { Grid } from "semantic-ui-react"; 5 | import PapersSidebar from "./Sidebar"; 6 | import JargonSearch from "./panel/search/JargonSearch"; 7 | import PaperSearch from "./panel/search/PaperSearch"; 8 | import MapCanvas from "./panel/map/Map"; 9 | import "./Panel.css"; 10 | 11 | export const PanelTabs = { 12 | JARGON_SEARCH: "jargon", 13 | PAPER_SEARCH: "paper", 14 | }; 15 | 16 | export default class PapersPanel extends Component { 17 | /** Component defining the main section div (sidebar + searchbar + map) */ 18 | 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | chosenTab: PanelTabs.PAPER_SEARCH, 23 | jargonTabProperties: { 24 | extras: {}, 25 | papers: [], 26 | }, 27 | searchTabProperties: { 28 | papers: [], 29 | }, 30 | }; 31 | 32 | // Tab choosing related functions 33 | this.getChosenTab = this.getChosenTab.bind(this); 34 | this.setJargonTab = this.setJargonTab.bind(this); 35 | this.setSearchTab = this.setSearchTab.bind(this); 36 | 37 | // Papers related getters and setters 38 | this.getJargonTabExtras = this.getJargonTabExtras.bind(this); 39 | this.getJargonTabPapers = this.getJargonTabPapers.bind(this); 40 | this.setJargonTabPapers = this.setJargonTabPapers.bind(this); 41 | this.getSearchTabPapers = this.getSearchTabPapers.bind(this); 42 | this.setSearchTabPapers = this.setSearchTabPapers.bind(this); 43 | } 44 | 45 | getChosenTab() { 46 | return this.state.chosenTab; 47 | } 48 | 49 | setJargonTab() { 50 | this.setState({ chosenTab: PanelTabs.JARGON_SEARCH }); 51 | } 52 | 53 | setSearchTab() { 54 | this.setState({ chosenTab: PanelTabs.PAPER_SEARCH }); 55 | } 56 | 57 | getJargonTabExtras() { 58 | return this.state.jargonTabProperties.extras; 59 | } 60 | 61 | getJargonTabPapers() { 62 | return this.state.jargonTabProperties.papers; 63 | } 64 | 65 | setJargonTabPapers(papers, extras) { 66 | this.setState(prevState => ({ 67 | ...prevState, 68 | jargonTabProperties: { papers: papers, extras: extras }, 69 | })); 70 | } 71 | 72 | getSearchTabPapers() { 73 | return this.state.searchTabProperties.papers; 74 | } 75 | 76 | setSearchTabPapers(papers) { 77 | this.setState(prevState => ({ 78 | ...prevState, 79 | searchTabProperties: { papers: papers }, 80 | })); 81 | } 82 | 83 | renderTabHeader() { 84 | switch (this.state.chosenTab) { 85 | case PanelTabs.JARGON_SEARCH: 86 | return ; 87 | case PanelTabs.PAPER_SEARCH: 88 | return ; 89 | default: 90 | return ; 91 | } 92 | } 93 | 94 | render() { 95 | return ( 96 | 97 | 98 | 103 | 104 | 105 | 106 | 107 | {this.renderTabHeader()} 108 | 109 | 110 | 115 | 116 | 117 | 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/scenes/papers/components/Sidebar.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { Component } from "react"; 4 | import { Icon, Menu } from "semantic-ui-react"; 5 | import { PanelTabs } from "./Panel"; 6 | 7 | export default class PapersSidebar extends Component { 8 | /** Component defining the main section sidebar to change between tabs */ 9 | 10 | isSearchActive() { 11 | return this.props.getChosenTab() === PanelTabs.PAPER_SEARCH; 12 | } 13 | 14 | isJargonActive() { 15 | return this.props.getChosenTab() === PanelTabs.JARGON_SEARCH; 16 | } 17 | 18 | render() { 19 | const { setJargonTab, setSearchTab } = this.props; 20 | 21 | return ( 22 | 23 | 28 | 29 | 30 | 35 | 36 | 37 | 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/scenes/papers/components/header/HeaderModal.css: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | .papers-header-info { 4 | cursor: pointer; 5 | } 6 | -------------------------------------------------------------------------------- /src/scenes/papers/components/header/HeaderModal.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { Component } from "react"; 4 | import { Header, Image, Modal, Segment } from "semantic-ui-react"; 5 | import Logo from "../../images/DS3_logo.png"; 6 | import "./HeaderModal.css"; 7 | 8 | export default class HeaderModal extends Component { 9 | /** Component defining the center overlaid box upon information request */ 10 | 11 | render() { 12 | return ( 13 | 16 | Info 17 | 18 | } 19 | > 20 | Dialect Map 21 | 22 | 23 | 24 | 25 |
Project description
26 |

27 | Dialect Map is a project initially supported by the NYU 28 | Data Science and Software Services initiative 29 | 34 | {" (DS3)"} 35 | 36 | , that compares jargon areas of influence across multiple 37 | science domains. For visualization purposes, it uses the 38 | representation of ArXiv stored papers provided by the 39 | PaperScape project. 40 |

41 |

42 | This web client has been created with the help of 43 | 48 | {" Rob J. Knegjens"} 49 | 50 | , one of the original PaperScape authors. 51 |

52 |
53 |
54 |
55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/scenes/papers/components/panel/map/Map.css: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | .leaflet-container { 4 | background-color: #000000; 5 | } 6 | 7 | .map-component { 8 | height: 100%; 9 | width: 100%; 10 | position: absolute; 11 | border-radius: 5px; 12 | } 13 | 14 | .panel-body-map-label { 15 | color: #FFFFFF; 16 | font-size: 14px; 17 | font-weight: bold; 18 | font-family: Arial, sans-serif; 19 | text-align: center; 20 | line-height: normal; 21 | white-space: nowrap; 22 | } 23 | 24 | .panel-body-map-info { 25 | top: 10px; 26 | right: 10px; 27 | width: 320px !important; /* Necessary flag */ 28 | z-index: 450; 29 | cursor: default; 30 | position: absolute !important; /* Necessary flag */ 31 | background: #FFFFFF !important; /* Necessary flag */ 32 | } 33 | -------------------------------------------------------------------------------- /src/scenes/papers/components/panel/map/Map.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { Component } from "react"; 4 | import config from "../../../../../config"; 5 | import { JargonColors } from "../search/JargonSearch"; 6 | import MapLayerControl from "./MapLayerControl"; 7 | import MapSelectPaper from "./MapSelectPaper"; 8 | import { Circle, MapContainer } from "react-leaflet"; 9 | import { CRS } from "leaflet"; 10 | import "leaflet/dist/leaflet.css"; 11 | import "./Map.css"; 12 | 13 | export default class MapCanvas extends Component { 14 | /** Component containing all the map related information / rendering */ 15 | 16 | constructor(props) { 17 | super(props); 18 | 19 | // Set up at render() time 20 | this.map = null; 21 | 22 | // Necessary binding in order to access parent functions 23 | this.calcJargonColor = this.calcJargonColor.bind(this); 24 | 25 | // Necessary binding in order to pass these functions to children 26 | this.getMap = this.getMap.bind(this); 27 | this.viewToWorld = this.viewToWorld.bind(this); 28 | this.worldToView = this.worldToView.bind(this); 29 | } 30 | 31 | getMap() { 32 | return this.map; 33 | } 34 | 35 | setMap(map) { 36 | if (this.map === null) { 37 | this.map = map; 38 | } 39 | } 40 | 41 | convertRadius(radius) { 42 | return radius * config.worldToViewScale; 43 | } 44 | 45 | convertRGBColor(color) { 46 | return `rgb(${color.r}, ${color.g}, ${color.b})`; 47 | } 48 | 49 | calcJargonColor(paperId) { 50 | let jargonExtras = this.props.getJargonExtras(); 51 | let paperFreqs = jargonExtras.freqByPaper[paperId]; 52 | 53 | let [freqA, freqB] = Object.values(paperFreqs); 54 | let [colorA, colorB] = JargonColors.map(c => c.rgb); 55 | 56 | if (freqA === 0) { 57 | return this.convertRGBColor(colorB); 58 | } 59 | if (freqB === 0) { 60 | return this.convertRGBColor(colorA); 61 | } 62 | 63 | let colorARatio = freqB > freqA ? freqA / freqB : 1 - freqB / freqA; 64 | let colorBRatio = freqA > freqB ? freqB / freqA : 1 - freqA / freqB; 65 | 66 | return this.convertRGBColor({ 67 | r: colorARatio * colorA.r + colorBRatio * colorB.r, 68 | g: colorARatio * colorA.g + colorBRatio * colorB.g, 69 | b: colorARatio * colorA.b + colorBRatio * colorB.b, 70 | }); 71 | } 72 | 73 | worldToView(world_X, world_Y) { 74 | // Leaflet considers [Y, X] not [X, Y] 75 | return [ 76 | -1 * (world_Y - config.worldMinY) * config.worldToViewScale, 77 | +1 * (world_X - config.worldMinX) * config.worldToViewScale, 78 | ]; 79 | } 80 | 81 | viewToWorld(view_X, view_Y) { 82 | // PaperScape considers [X, Y] not [Y, X] 83 | return [ 84 | +1 * view_X * config.viewToWorldScale + config.worldMinX, 85 | -1 * view_Y * config.viewToWorldScale + config.worldMinY, 86 | ]; 87 | } 88 | 89 | render() { 90 | const jargonPapers = this.props.getJargonPapers(); 91 | const searchPapers = this.props.getSearchPapers(); 92 | 93 | return ( 94 | // In react-leaflet v2, "ref" is used to obtain the created map instance 95 | // In react-leaflet v3, "whenCreated" is used to obtain the created map instance 96 | this.setMap(map)} 107 | > 108 | 113 | 114 | 119 | 120 | {jargonPapers.map((paper, index) => ( 121 | 127 | ))} 128 | 129 | {searchPapers.map((paper, index) => ( 130 | 136 | ))} 137 | 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/scenes/papers/components/panel/map/MapInfoBox.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { Component } from "react"; 4 | import { Button, Card, Icon, List } from "semantic-ui-react"; 5 | 6 | export default class MapInfoBox extends Component { 7 | /** Component defining the information box upon paper selection */ 8 | 9 | buildArxivLink(arxivID) { 10 | return "https://arxiv.org/pdf/" + arxivID; 11 | } 12 | 13 | render() { 14 | const { getPaperInfo, hidePaperInfo } = this.props; 15 | const paperInfo = getPaperInfo(); 16 | const arxivLink = this.buildArxivLink(paperInfo.arxivID); 17 | 18 | return ( 19 | 20 | 23 | 24 | 25 | {paperInfo.getTitleForView()} 26 | {paperInfo.getAuthorsForView()} 27 | 28 | {paperInfo.getPublisherForView()} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {paperInfo.numRefs} references 40 | 41 | 42 | 43 | {paperInfo.numCits} citations 44 | 45 | 46 | 47 | 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/scenes/papers/components/panel/map/MapLayerControl.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { Component } from "react"; 4 | import config from "../../../../../config"; 5 | import MapLabelsCtl from "../../../controllers/paperscape/MapLabels"; 6 | import MapTileLayer from "./MapTileLayer"; 7 | import { FeatureGroup, LayersControl, Marker } from "react-leaflet"; 8 | import { divIcon } from "leaflet"; 9 | 10 | export default class MapLayerControl extends Component { 11 | /** Component defining the box with the layer control options */ 12 | 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | labels: [], 17 | }; 18 | 19 | // Necessary binding in order to access parent functions 20 | this.loadLabels = this.loadLabels.bind(this); 21 | } 22 | 23 | buildLabel(label) { 24 | let labelLevels = label.split(","); 25 | labelLevels = labelLevels.filter(s => s !== ""); 26 | labelLevels = labelLevels.slice(0, 3); 27 | labelLevels = labelLevels.join("
"); 28 | 29 | return labelLevels; 30 | } 31 | 32 | filterWorldLabels(label, northEast, southWest) { 33 | return ( 34 | label.x >= southWest[0] && 35 | label.x <= northEast[0] && 36 | label.y >= northEast[1] && 37 | label.y <= southWest[1] 38 | ); 39 | } 40 | 41 | async loadLabels() { 42 | let map = this.props.getMap(); 43 | 44 | let viewCenter = map.getCenter(); 45 | let worldCenter = this.props.viewToWorld(viewCenter.lng, viewCenter.lat); 46 | let currentZoom = map.getZoom(); 47 | let roundedZoom = Math.floor(currentZoom); 48 | 49 | let labels = await MapLabelsCtl.fetchLabels(roundedZoom, worldCenter); 50 | 51 | let viewBounds = map.getBounds(); 52 | let worldNorthEast = this.props.viewToWorld( 53 | viewBounds._northEast.lng, 54 | viewBounds._northEast.lat 55 | ); 56 | let worldSouthWest = this.props.viewToWorld( 57 | viewBounds._southWest.lng, 58 | viewBounds._southWest.lat 59 | ); 60 | 61 | let worldLabels = labels.filter(label => 62 | this.filterWorldLabels(label, worldNorthEast, worldSouthWest) 63 | ); 64 | 65 | this.setState({ 66 | labels: worldLabels, 67 | }); 68 | } 69 | 70 | render() { 71 | const { worldToView } = this.props; 72 | const { labels } = this.state; 73 | 74 | return ( 75 | 76 | 77 | 81 | 82 | 83 | 87 | 88 | 89 | 90 | {labels.map((label, index) => ( 91 | 99 | ))} 100 | 101 | 102 | 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/scenes/papers/components/panel/map/MapSelectPaper.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { Component } from "react"; 4 | import ClickHandler from "./events/MapClickEvent"; 5 | import MapInfoBox from "./MapInfoBox"; 6 | import { Circle } from "react-leaflet"; 7 | 8 | export default class MapSelectPaper extends Component { 9 | /** Component defining the paper selection elements */ 10 | 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | paperInfoVisible: false, 15 | paperInfo: null, 16 | paperPos: null, 17 | }; 18 | 19 | // Necessary binding in order to pass these functions to children 20 | this.getPaperInfo = this.getPaperInfo.bind(this); 21 | this.hidePaperInfo = this.hidePaperInfo.bind(this); 22 | this.updateSelected = this.updateSelected.bind(this); 23 | } 24 | 25 | getPaperInfo() { 26 | return this.state.paperInfo; 27 | } 28 | 29 | hidePaperInfo() { 30 | this.setState({ paperInfoVisible: false }); 31 | } 32 | 33 | updateSelected(paperPos, paperInfo) { 34 | this.setState({ 35 | paperInfoVisible: true, 36 | paperInfo: paperInfo, 37 | paperPos: paperPos, 38 | }); 39 | } 40 | 41 | render() { 42 | const { convertRadius, viewToWorld, worldToView } = this.props; 43 | const { paperPos } = this.state; 44 | 45 | return ( 46 |
47 | 51 | {this.state.paperInfoVisible ? ( 52 | 56 | ) : null} 57 | {this.state.paperPos !== null ? ( 58 | 63 | ) : null} 64 |
65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/scenes/papers/components/panel/map/MapTileLayer.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import config from "../../../../../config"; 4 | import { TileLayer } from "leaflet"; 5 | import { createLayerComponent, updateGridLayer } from "@react-leaflet/core"; 6 | 7 | class MapTileLayer { 8 | /** Class overriding the default Leaflet TileLayer functionality */ 9 | 10 | static getCustomOptions() { 11 | return { 12 | attribution: config.tilesAttrib, 13 | minZoom: config.mapZoomMinimum, 14 | maxZoom: config.mapZoomMaximum, 15 | subdomains: config.mapSubdomains, 16 | tileSize: config.worldTileSize, 17 | }; 18 | } 19 | 20 | static customGetTileUrl(coords) { 21 | coords.x += 1; 22 | coords.y += 1; 23 | return this.defaultGetTileUrl(coords); 24 | } 25 | 26 | static createTileLayer(props, context) { 27 | let options = MapTileLayer.getCustomOptions(); 28 | let layer = new TileLayer(props.tilesURL, options); 29 | 30 | // Override default in order to correct zoom drift 31 | layer.defaultGetTileUrl = layer.getTileUrl; 32 | layer.getTileUrl = MapTileLayer.customGetTileUrl; 33 | layer.on("load", props.loadLabels); 34 | 35 | return { 36 | instance: layer, 37 | context: context, 38 | }; 39 | } 40 | 41 | static updateTileLayer(instance, props, prevProps) { 42 | updateGridLayer(instance, props, prevProps); 43 | 44 | if (props.url !== prevProps.url) { 45 | instance.setUrl(props.url); 46 | } 47 | } 48 | } 49 | 50 | export default createLayerComponent( 51 | MapTileLayer.createTileLayer, 52 | MapTileLayer.updateTileLayer 53 | ); 54 | -------------------------------------------------------------------------------- /src/scenes/papers/components/panel/map/events/MapClickEvent.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import PaperInfoCtl from "../../../../controllers/paperscape/PaperInfo"; 4 | import PaperPositionCtl from "../../../../controllers/paperscape/PaperPosition"; 5 | import { useMapEvent } from "react-leaflet"; 6 | 7 | export default function setClickHandler({ viewToWorld, updateSelected }) { 8 | /** 9 | * Function setting the map handler for the click event. 10 | * This function must be called within a React render() context. 11 | */ 12 | 13 | useMapEvent("click", e => 14 | checkMapClick(e) 15 | .then(event => viewToWorld(event.latlng.lng, event.latlng.lat)) 16 | .then(coords => clickToPaper(coords)) 17 | .then(paper => updateSelected(paper.pos, paper.info)) 18 | .catch(err => console.log(err.message)) 19 | ); 20 | 21 | return null; 22 | } 23 | 24 | async function checkMapClick(event) { 25 | // Order matters. 'Undefined' checking must go from 26 | // the most specific to the least specific property 27 | let clickedClass = 28 | event.originalEvent.target.className.baseVal || 29 | event.originalEvent.target.className; 30 | 31 | let isDivStrings = typeof clickedClass === "string"; 32 | let isDivLeaflet = clickedClass.includes("leaflet"); 33 | 34 | if (isDivStrings && isDivLeaflet === false) { 35 | throw new Error("Clicked outside"); 36 | } else { 37 | return event; 38 | } 39 | } 40 | 41 | async function clickToPaper(coords) { 42 | let paperPos = await PaperPositionCtl.fetchPaperPos(coords[0], coords[1]); 43 | if (paperPos === null) { 44 | throw new Error("Unknown paper coordinates"); 45 | } 46 | 47 | let paperInfo = await PaperInfoCtl.fetchPaperInfo(paperPos.id); 48 | if (paperInfo === null) { 49 | throw new Error("Unknown paper ID"); 50 | } 51 | 52 | return { pos: paperPos, info: paperInfo }; 53 | } 54 | -------------------------------------------------------------------------------- /src/scenes/papers/components/panel/search/JargonSearch.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { Component } from "react"; 4 | import { Button, Icon, Image, Input, Menu, Segment } from "semantic-ui-react"; 5 | import JargonSearchCtl from "../../../controllers/backend/JargonSearch"; 6 | import MetricSearchCtl from "../../../controllers/backend/MetricSearch"; 7 | import PaperSearchCtl from "../../../controllers/paperscape/PaperSearch"; 8 | import PaperSearchPositionCtl from "../../../controllers/paperscape/PaperSearchPosition"; 9 | import "./Search.css"; 10 | 11 | // prettier-ignore 12 | export const JargonColors = [ 13 | {key: "blue", text: "blue", rgb: {r: 0, g: 0, b: 255}}, 14 | {key: "red", text: "red", rgb: {r: 255, g: 0, b: 0}}, 15 | ]; 16 | 17 | export default class JargonSearch extends Component { 18 | /** Component to define the jargon terms comparison across papers */ 19 | 20 | constructor(props) { 21 | super(props); 22 | this.state = { 23 | searchedJargonA: "", 24 | searchedJargonB: "", 25 | }; 26 | 27 | // Necessary binding in order to access parent functions 28 | this.searchPapersAndExtras = this.searchPapersAndExtras.bind(this); 29 | } 30 | 31 | updateJargonA(event) { 32 | this.setState({ 33 | searchedJargonA: event.target.value, 34 | }); 35 | } 36 | 37 | updateJargonB(event) { 38 | this.setState({ 39 | searchedJargonB: event.target.value, 40 | }); 41 | } 42 | 43 | async queryJargonIds(jargons) { 44 | let promises = jargons.map(j => JargonSearchCtl.fetchJargonID(j)); 45 | let results = await Promise.all(promises); 46 | return results.filter(id => id !== null); 47 | } 48 | 49 | async queryMetrics(jargonIds) { 50 | let promises = jargonIds.map(id => MetricSearchCtl.fetchLatestMetrics(id)); 51 | let results = await Promise.all(promises); 52 | return results.flat(); 53 | } 54 | 55 | async queryPaperIds(arxivIds) { 56 | let promises = arxivIds.map(id => PaperSearchCtl.fetchPapersIDs("saxm", id)); 57 | let results = await Promise.all(promises); 58 | return results.flat(); 59 | } 60 | 61 | async searchFrequenciesByPaper() { 62 | let jargons = [this.state.searchedJargonA, this.state.searchedJargonB]; 63 | let jargonIds = await this.queryJargonIds(jargons); 64 | let metrics = await this.queryMetrics(jargonIds); 65 | 66 | let emptyFreqs = {}; 67 | jargonIds.forEach(id => (emptyFreqs[id] = 0)); 68 | 69 | let papersFreqs = {}; 70 | metrics.forEach(m => (papersFreqs[m.arxivID] = { ...emptyFreqs })); 71 | metrics.forEach(m => (papersFreqs[m.arxivID][m.jargonID] = m.absFreq)); 72 | 73 | return papersFreqs; 74 | } 75 | 76 | async searchPapers(arxivIds) { 77 | let paperIds = await this.queryPaperIds(arxivIds); 78 | if (paperIds.length === 0) { 79 | return []; 80 | } 81 | 82 | return await PaperSearchPositionCtl.fetchPapersPos(paperIds); 83 | } 84 | 85 | async searchPapersAndExtras() { 86 | let freqs = await this.searchFrequenciesByPaper(); 87 | let axvIds = Object.keys(freqs); 88 | let papers = await this.searchPapers(axvIds); 89 | this.props.setJargonPapers(papers, { freqByPaper: freqs }); 90 | } 91 | 92 | render() { 93 | return ( 94 | 95 | 96 | 97 | 98 | 104 | 105 | Jargon A: 106 | this.updateJargonA(event)} 110 | /> 111 | 112 | 113 | 114 | 120 | 121 | Jargon B: 122 | this.updateJargonB(event)} 126 | /> 127 | 128 | 129 | 130 | 131 | 134 | 135 | 136 | 137 | 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/scenes/papers/components/panel/search/PaperSearch.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { Component } from "react"; 4 | import PaperSearchCtl from "../../../controllers/paperscape/PaperSearch"; 5 | import PaperSearchPositionCtl from "../../../controllers/paperscape/PaperSearchPosition"; 6 | import { Button, Dropdown, Icon, Image, Input, Menu, Segment } from "semantic-ui-react"; 7 | import "./Search.css"; 8 | 9 | // prettier-ignore 10 | export const SearchOptions = [ 11 | {key: "arxiv", text: "Arxiv ID", value: "saxm"}, 12 | {key: "author", text: "Author", value: "sau"}, 13 | {key: "keyword", text: "Keyword", value: "skw"}, 14 | {key: "title", text: "Title", value: "sti"}, 15 | {key: "new-papers", text: "New papers", value: "sca"}, 16 | ]; 17 | 18 | export default class PaperSearch extends Component { 19 | /** Component to define the search query to the PaperScape API */ 20 | 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | paperSearch: "", 25 | paperSearchType: "", 26 | }; 27 | 28 | // Necessary binding in order to access parent functions 29 | this.searchPapers = this.searchPapers.bind(this); 30 | } 31 | 32 | updateSearch(event) { 33 | this.setState({ 34 | paperSearch: event.target.value, 35 | }); 36 | } 37 | 38 | updateSearchType(change) { 39 | this.setState({ 40 | paperSearchType: change.value, 41 | }); 42 | } 43 | 44 | async searchPapers() { 45 | let ids = await PaperSearchCtl.fetchPapersIDs( 46 | this.state.paperSearchType, 47 | this.state.paperSearch 48 | ); 49 | if (ids.length === 0) { 50 | return null; 51 | } 52 | 53 | let papers = await PaperSearchPositionCtl.fetchPapersPos(ids); 54 | this.props.setSearchPapers(papers); 55 | } 56 | 57 | render() { 58 | return ( 59 | 60 | 61 | 62 | 63 | 64 | 65 | Search: 66 | this.updateSearch(event)} 70 | /> 71 | 72 | 73 | 74 | this.updateSearchType(change)} 80 | /> 81 | 82 | 83 | 84 | 85 | 88 | 89 | 90 | 91 | 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/scenes/papers/components/panel/search/Search.css: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | .search-container { 4 | padding-top: 5px !important; /* Necessary flag */ 5 | padding-bottom: 5px !important; /* Necessary flag */ 6 | } 7 | 8 | .search-menu-item { 9 | width: 28%; 10 | height: 50px !important; /* Necessary flag */ 11 | } 12 | 13 | .search-menu-text { 14 | width: 150px; 15 | margin-left: 5px; 16 | margin-right: 5px; 17 | } 18 | 19 | .search-start-container { 20 | padding: 0 !important; /* Necessary flag */ 21 | } 22 | 23 | .search-menu-dropdown { 24 | z-index: 1000 !important; /* Necessary flag */ 25 | } 26 | -------------------------------------------------------------------------------- /src/scenes/papers/controllers/backend/JargonSearch.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import config from "../../../../config"; 4 | 5 | export default class JargonSearchCtl { 6 | /** Controller defining Jargon search queries to our own backend */ 7 | 8 | static fetchJargonID(searchJargon) { 9 | // prettier-ignore 10 | let url = config.dialectMapURL 11 | + "/jargon/string/" 12 | + encodeURIComponent(searchJargon); 13 | 14 | return fetch(url, {}) 15 | .then(resp => resp.json()) 16 | .then(json => this._handleJargonSearchResp(json)) 17 | .catch(err => console.log(err)); 18 | } 19 | 20 | static _handleJargonSearchResp(json) { 21 | let jargonID = null; 22 | 23 | try { 24 | jargonID = json["jargon_id"]; 25 | } catch (error) { 26 | console.log(error); 27 | } 28 | 29 | return jargonID; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/scenes/papers/controllers/backend/MetricSearch.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import config from "../../../../config"; 4 | import PaperJargonMetric from "../../models/PaperMetric"; 5 | 6 | export default class MetricSearchCtl { 7 | /** Controller defining metrics search queries to our own backend */ 8 | 9 | static fetchLatestMetrics(jargonID) { 10 | // prettier-ignore 11 | let url = config.dialectMapURL 12 | + `/paper-metrics` 13 | + `/jargon` 14 | + `/${encodeURIComponent(jargonID)}` 15 | + `/latest` 16 | 17 | return fetch(url, {}) 18 | .then(resp => resp.json()) 19 | .then(json => this._handleMetricSearchResp(json)) 20 | .catch(err => console.log(err)); 21 | } 22 | 23 | static _handleMetricSearchResp(json) { 24 | let metrics = []; 25 | 26 | try { 27 | metrics = json.map(metric => new PaperJargonMetric(metric)); 28 | } catch (error) { 29 | console.log(error); 30 | } 31 | 32 | return metrics; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/scenes/papers/controllers/paperscape/MapConfig.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import config from "../../../../config"; 4 | 5 | const mapConfigRespPrefix = "world_index("; 6 | const mapConfigRespSuffix = ")"; 7 | 8 | export default class MapConfigCtl { 9 | /** Controller to fetch the initial map configuration from Paperscape */ 10 | 11 | static fetchConfig() { 12 | console.log("Loading PaperScape configuration..."); 13 | 14 | return fetch(config.worldConfigURL, {}) 15 | .then(resp => resp.text()) 16 | .then(text => this._handlePaperIDResp(text)); 17 | } 18 | 19 | static _calcWorldToViewScale(tilePixels, tilePixelsAtZ0) { 20 | return tilePixels / tilePixelsAtZ0; 21 | } 22 | 23 | static _calcViewToWorldScale(tilePixels, tilePixelsAtZ0) { 24 | return tilePixelsAtZ0 / tilePixels; 25 | } 26 | 27 | static _handlePaperIDResp(text) { 28 | let body = this._pruneWorldConfigResp(text); 29 | let conf = JSON.parse(body); 30 | 31 | this._updateConfig(conf); 32 | console.log("PaperScape configuration loaded"); 33 | } 34 | 35 | static _pruneWorldConfigResp(body) { 36 | return body.slice( 37 | +1 * mapConfigRespPrefix.length, 38 | -1 * mapConfigRespSuffix.length 39 | ); 40 | } 41 | 42 | static _updateConfig(conf) { 43 | config.worldMinX = conf["xmin"]; 44 | config.worldMaxX = conf["xmax"]; 45 | config.worldMinY = conf["ymin"]; 46 | config.worldMaxY = conf["ymax"]; 47 | config.worldTileSize = conf["pixelw"]; 48 | 49 | /* IMPORTANT NOTE: 50 | * 51 | * Leaflet "Simple" CRS supposes a 1:1 ratio 52 | * Between tile pixels and world pixels at zoom 0. 53 | * As it is not the case, scaling need to be performed 54 | */ 55 | config.viewToWorldScale = this._calcViewToWorldScale( 56 | conf["pixelw"], 57 | conf["tilings"][0]["tw"] 58 | ); 59 | config.worldToViewScale = this._calcWorldToViewScale( 60 | conf["pixelw"], 61 | conf["tilings"][0]["tw"] 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/scenes/papers/controllers/paperscape/MapLabels.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import config from "../../../../config"; 4 | 5 | const mapLabelsRespPrefix = "lz_Z_X_Y("; 6 | const mapLabelsRespSuffix = ")"; 7 | 8 | export default class MapLabelsCtl { 9 | /** Controller defining the map labels queries to the Paperscape API */ 10 | 11 | static fetchLabels(zoomLevel, centerPos) { 12 | let labelSpec = config.worldLabels[zoomLevel]; 13 | let labelsXTile = this._getLabelTile( 14 | centerPos[0], 15 | config.worldMinX, 16 | config.worldMaxX, 17 | labelSpec.nx 18 | ); 19 | let labelsYTile = this._getLabelTile( 20 | centerPos[1], 21 | config.worldMinY, 22 | config.worldMaxY, 23 | labelSpec.ny 24 | ); 25 | 26 | // prettier-ignore 27 | let url = config.labelsJsonHost 28 | + "/" + zoomLevel 29 | + "/" + labelsXTile 30 | + "/" + labelsYTile 31 | + ".json"; 32 | 33 | return fetch(url, {}) 34 | .then(resp => resp.text()) 35 | .then(text => this._handleLabelsResp(text)) 36 | .catch(err => console.log(err)); 37 | } 38 | 39 | static _getLabelTile(coord, coordMin, coordMax, tilesNum) { 40 | let chunkSize = (coordMax - coordMin) / tilesNum; 41 | let highBound = coordMin + chunkSize; 42 | let tileIndex = 1; 43 | 44 | while (coord >= highBound) { 45 | highBound += chunkSize; 46 | tileIndex += 1; 47 | } 48 | 49 | return tileIndex; 50 | } 51 | 52 | static _handleLabelsResp(text) { 53 | let body = this._pruneLabelsResp(text); 54 | let json = JSON.parse(body); 55 | return json["lbls"]; 56 | } 57 | 58 | static _pruneLabelsResp(body) { 59 | return body.slice( 60 | +1 * mapLabelsRespPrefix.length, 61 | -1 * mapLabelsRespSuffix.length 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/scenes/papers/controllers/paperscape/PaperInfo.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import config from "../../../../config"; 4 | import PaperInfo from "../../models/PaperInfo"; 5 | 6 | const paperInfoRespPrefix = "("; 7 | const paperInfoRespSuffix = ")\n"; 8 | 9 | export default class PaperInfoCtl { 10 | /** Controller defining the paper info queries to the Paperscape API */ 11 | 12 | static fetchPaperInfo(paperID) { 13 | // prettier-ignore 14 | let url = config.papersDataURL 15 | + "?callback=" 16 | + "&flags[]=1" 17 | + "&gdata[]=" + paperID; 18 | 19 | return fetch(url, {}) 20 | .then(resp => resp.text()) 21 | .then(text => this._handlePaperInfoResp(text)); 22 | } 23 | 24 | static _handlePaperInfoResp(text) { 25 | let paperInfo = null; 26 | 27 | try { 28 | let body = this._prunePaperInfoResp(text); 29 | let json = JSON.parse(body); 30 | let data = json["r"]["papr"][0]; 31 | paperInfo = new PaperInfo(data); 32 | } catch (error) { 33 | console.log(error); 34 | } 35 | 36 | return paperInfo; 37 | } 38 | 39 | static _prunePaperInfoResp(body) { 40 | return body.slice( 41 | +1 * paperInfoRespPrefix.length, 42 | -1 * paperInfoRespSuffix.length 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/scenes/papers/controllers/paperscape/PaperPosition.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import config from "../../../../config"; 4 | import PaperPosition from "../../models/PaperPosition"; 5 | 6 | const paperPositionRespPrefix = "("; 7 | const paperPositionRespSuffix = ")\n"; 8 | 9 | export default class PaperPositionCtl { 10 | /** Controller defining the paper coordinates queries to the Paperscape API */ 11 | 12 | static fetchPaperPos(X_pos, Y_pos) { 13 | // prettier-ignore 14 | let url = config.papersDataURL 15 | + "?callback=" 16 | + "&tbl=" 17 | + "&ml2p[]=" + X_pos 18 | + "&ml2p[]=" + Y_pos; 19 | 20 | return fetch(url, {}) 21 | .then(resp => resp.text()) 22 | .then(text => this._handlePaperPosResp(text)); 23 | } 24 | 25 | static _handlePaperPosResp(text) { 26 | let paperPos = null; 27 | 28 | try { 29 | let body = this._prunePaperPosResp(text); 30 | let json = JSON.parse(body); 31 | let data = json["r"]; 32 | paperPos = new PaperPosition(data); 33 | } catch (error) { 34 | console.log(error); 35 | } 36 | 37 | return paperPos; 38 | } 39 | 40 | static _prunePaperPosResp(body) { 41 | return body.slice( 42 | +1 * paperPositionRespPrefix.length, 43 | -1 * paperPositionRespSuffix.length 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/scenes/papers/controllers/paperscape/PaperSearch.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import config from "../../../../config"; 4 | 5 | const paperSearchRespPrefix = "("; 6 | const paperSearchRespSuffix = ")\n"; 7 | 8 | export default class PaperSearchCtl { 9 | /** Controller defining the paper search queries to the Paperscape API */ 10 | 11 | static fetchPapersIDs(searchKey, searchValue) { 12 | // prettier-ignore 13 | let url = config.papersDataURL 14 | + this._buildRequestParams(searchKey, searchValue); 15 | 16 | return fetch(url, {}) 17 | .then(resp => resp.text()) 18 | .then(text => this._handlePaperSearchResp(text)); 19 | } 20 | 21 | static _buildRequestParams(searchKey, searchValue) { 22 | let params = "?callback="; 23 | 24 | switch (searchKey) { 25 | case "saxm": 26 | params += "&saxm=" + searchValue; 27 | break; 28 | case "sau": 29 | params += "&sau=" + searchValue; 30 | break; 31 | case "skw": 32 | params += "&skw=" + searchValue; 33 | break; 34 | case "sti": 35 | params += "&sti=" + searchValue; 36 | break; 37 | case "sca": 38 | params += "&sca=" + searchValue + "&fd=1&td=0"; 39 | break; 40 | default: 41 | params += "&skw=" + searchValue; 42 | break; 43 | } 44 | 45 | return params; 46 | } 47 | 48 | static _handlePaperSearchResp(text) { 49 | let paperIDs = []; 50 | 51 | try { 52 | let body = this._prunePaperSearchResp(text); 53 | let json = JSON.parse(body); 54 | let data = json["r"]; 55 | paperIDs = data.map(paper => paper.id); 56 | } catch (error) { 57 | console.log(error); 58 | } 59 | 60 | return paperIDs; 61 | } 62 | 63 | static _prunePaperSearchResp(body) { 64 | return body.slice( 65 | +1 * paperSearchRespPrefix.length, 66 | -1 * paperSearchRespSuffix.length 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/scenes/papers/controllers/paperscape/PaperSearchPosition.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import config from "../../../../config"; 4 | import PaperPosition from "../../models/PaperPosition"; 5 | 6 | export default class PaperSearchPositionCtl { 7 | /** Controller defining the paper position search queries to the Paperscape API */ 8 | 9 | static fetchPapersPos(paperIDs) { 10 | let url = config.papersDataURL; 11 | let params = { 12 | method: "POST", 13 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 14 | body: this._buildRequestBody(paperIDs), 15 | }; 16 | 17 | return fetch(url, params) 18 | .then(resp => resp.json()) 19 | .then(json => this._handlePaperPosResp(json)); 20 | } 21 | 22 | static _buildRequestBody(paperIDs) { 23 | // prettier-ignore 24 | paperIDs = paperIDs.map((id) => "&mp2l[]=" + id); 25 | paperIDs = paperIDs.join(""); 26 | return "tbl=" + paperIDs; 27 | } 28 | 29 | static _handlePaperPosResp(json) { 30 | let paperPos = []; 31 | 32 | try { 33 | let data = json["r"]; 34 | paperPos = data.map(paper => new PaperPosition(paper)); 35 | } catch (error) { 36 | console.log(error); 37 | } 38 | 39 | return paperPos; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/scenes/papers/images/DS3_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialect-map/dialect-map-ui/0791322bed1f4e57c64de4561cc30b0ca4d0440e/src/scenes/papers/images/DS3_logo.png -------------------------------------------------------------------------------- /src/scenes/papers/models/PaperInfo.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | export default class PaperInfo { 4 | /** Model containing the paper information structure */ 5 | 6 | maxCharsPerField = 40; 7 | 8 | // prettier-ignore 9 | constructor(data) { 10 | // Some of these fields may not be in the API response 11 | this.title = data["titl"] || ""; 12 | this.auth = data["auth"] || ""; 13 | this.publ = data["publ"] || ""; 14 | this.arxivID = data["arxv"] || ""; 15 | this.numRefs = data["nr"]; 16 | this.numCits = data["nc"]; 17 | } 18 | 19 | getTitleForView() { 20 | let shortTitle = this.title.substring(0, this.maxCharsPerField); 21 | if (shortTitle.length < this.title.length) { 22 | shortTitle += "..."; 23 | } 24 | return shortTitle; 25 | } 26 | 27 | getAuthorsForView() { 28 | let shortAuthors = this.auth.substring(0, this.maxCharsPerField); 29 | if (shortAuthors.length < this.auth.length) { 30 | shortAuthors += "..."; 31 | } 32 | return shortAuthors; 33 | } 34 | 35 | getPublisherForView() { 36 | let shortPublisher = this.publ.substring(0, this.maxCharsPerField); 37 | if (shortPublisher.length < this.publ.length) { 38 | shortPublisher += "..."; 39 | } 40 | return shortPublisher; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/scenes/papers/models/PaperMetric.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | export default class PaperJargonMetric { 4 | /** Model containing the basic jargon metric structure */ 5 | 6 | // prettier-ignore 7 | constructor(data) { 8 | this.id = data["metric_id"]; 9 | this.jargonID = data["jargon_id"]; 10 | this.arxivID = data["arxiv_id"]; 11 | this.arxivRev = data["arxiv_rev"]; 12 | this.absFreq = data["abs_freq"]; 13 | this.relFreq = data["rel_freq"]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/scenes/papers/models/PaperPosition.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | export default class PaperPosition { 4 | /** Model containing the basic paper position structure */ 5 | 6 | // prettier-ignore 7 | constructor(data) { 8 | this.id = data["id"]; 9 | this.x = data["x"]; 10 | this.y = data["y"]; 11 | this.r = data["r"]; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === "localhost" || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === "[::1]" || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener("load", () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | "This web app is being served cache-first by a service " + 46 | "worker. To learn more, visit http://bit.ly/CRA-PWA" 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === "installed") { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | "New content is available and will be used when all " + 74 | "tabs for this page are closed. See http://bit.ly/CRA-PWA." 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log("Content is cached for offline use."); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error("Error during service worker registration:", error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get("content-type"); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf("javascript") === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log("No internet connection found. App is running in offline mode."); 124 | }); 125 | } 126 | 127 | export function unregister() { 128 | if ("serviceWorker" in navigator) { 129 | navigator.serviceWorker.ready.then(registration => { 130 | registration.unregister(); 131 | }); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/papers/Papers.test.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { render } from "@testing-library/react"; 4 | import MapConfigCtl from "../../src/scenes/papers/controllers/paperscape/MapConfig"; 5 | import PapersMap from "../../src/scenes/papers/Papers"; 6 | 7 | describe("Papers scene", () => { 8 | /** Set of tests for the Papers scene component */ 9 | 10 | beforeEach(() => { 11 | MapConfigCtl.fetchConfig = jest.fn().mockResolvedValue({}); 12 | }); 13 | 14 | test("Mounts zero components by default", () => { 15 | const { container } = render(); 16 | const numberChildren = container.children.length; 17 | expect(numberChildren).toBe(0); 18 | }); 19 | 20 | test("Mounts some components upon config loading", async () => { 21 | const { container } = render(); 22 | await MapConfigCtl.fetchConfig; 23 | const numberChildren = container.children.length; 24 | expect(numberChildren).toBeGreaterThan(0); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/papers/components/Header.test.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { render } from "@testing-library/react"; 4 | import PapersHeader from "../../../src/scenes/papers/components/Header"; 5 | 6 | describe("Header component", () => { 7 | /** Set of tests for the Header component */ 8 | 9 | test("Shows header title", () => { 10 | const { getByText } = render(); 11 | const headerTitle = getByText("Dialect Map"); 12 | expect(headerTitle).toBeDefined(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/papers/components/Panel.test.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import renderer from "react-test-renderer"; 4 | import PapersPanel, { PanelTabs } from "../../../src/scenes/papers/components/Panel"; 5 | 6 | describe("Panel component", () => { 7 | /** Set of tests for the Panel component */ 8 | 9 | test("Sets jargon search tab", () => { 10 | const wrapper = renderer.create(); 11 | const instance = wrapper.getInstance(); 12 | 13 | instance.state.chosenTab = null; 14 | instance.setJargonTab(); 15 | 16 | const currentTab = instance.state.chosenTab; 17 | expect(currentTab).toBe(PanelTabs.JARGON_SEARCH); 18 | }); 19 | 20 | test("Sets paper search tab", () => { 21 | const wrapper = renderer.create(); 22 | const instance = wrapper.getInstance(); 23 | 24 | instance.state.chosenTab = null; 25 | instance.setSearchTab(); 26 | 27 | const currentTab = instance.state.chosenTab; 28 | expect(currentTab).toBe(PanelTabs.PAPER_SEARCH); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/papers/components/Sidebar.test.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { render } from "@testing-library/react"; 4 | import PapersSidebar from "../../../src/scenes/papers/components/Sidebar"; 5 | 6 | describe("Sidebar component", () => { 7 | /** Set of tests for the Sidebar component */ 8 | 9 | test("Shows menu icons", () => { 10 | const { container } = render( {}} />); 11 | const sidebarIcons = container.getElementsByClassName("circular icon"); 12 | expect(sidebarIcons).toHaveLength(2); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/papers/components/header/HeaderModal.test.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { render, fireEvent } from "@testing-library/react"; 4 | import HeaderModal from "../../../../src/scenes/papers/components/header/HeaderModal"; 5 | 6 | describe("HeaderModal component", () => { 7 | /** Set of tests for the HeaderModal component */ 8 | 9 | test("Shows modal button", () => { 10 | const { getByText } = render(); 11 | const modalButton = getByText("Info"); 12 | expect(modalButton).toBeDefined(); 13 | }); 14 | 15 | test("Shows modal content upon click", async () => { 16 | const { getByText } = render(); 17 | const button = getByText("Info"); 18 | fireEvent.click(button); 19 | 20 | const modelContentTitle = getByText("Project description"); 21 | expect(modelContentTitle).toBeDefined(); 22 | }); 23 | 24 | test("Hides modal content prior click", async () => { 25 | const { queryByText } = render(); 26 | const modelContentTitle = queryByText("Project description"); 27 | expect(modelContentTitle).toBeNull(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/papers/components/panel/search/JargonSearch.test.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { render } from "@testing-library/react"; 4 | import renderer from "react-test-renderer"; 5 | import JargonSearch from "../../../../../src/scenes/papers/components/panel/search/JargonSearch"; 6 | import JargonSearchCtl from "../../../../../src/scenes/papers/controllers/backend/JargonSearch"; 7 | import MetricSearchCtl from "../../../../../src/scenes/papers/controllers/backend/MetricSearch"; 8 | import PaperSearchCtl from "../../../../../src/scenes/papers/controllers/paperscape/PaperSearch"; 9 | 10 | describe("JargonSearch component", () => { 11 | /** Set of tests for the JargonSearch component */ 12 | 13 | test("Updates search jargon A", () => { 14 | const wrapper = renderer.create(); 15 | const instance = wrapper.getInstance(); 16 | const testValue = "example"; 17 | const testEvent = { target: { value: testValue } }; 18 | 19 | instance.state.searchedJargonA = null; 20 | instance.updateJargonA(testEvent); 21 | 22 | const searchJargon = instance.state.searchedJargonA; 23 | expect(searchJargon).toBe(testValue); 24 | }); 25 | 26 | test("Updates search jargon B", () => { 27 | const wrapper = renderer.create(); 28 | const instance = wrapper.getInstance(); 29 | const testValue = "example"; 30 | const testEvent = { target: { value: testValue } }; 31 | 32 | instance.state.searchedJargonB = null; 33 | instance.updateJargonB(testEvent); 34 | 35 | const searchJargon = instance.state.searchedJargonB; 36 | expect(searchJargon).toBe(testValue); 37 | }); 38 | 39 | test("Gets Jargon IDs", async () => { 40 | JargonSearchCtl.fetchJargonID = jest.fn().mockResolvedValue(null); 41 | 42 | const wrapper = renderer.create(); 43 | const instance = wrapper.getInstance(); 44 | const results = await instance.queryJargonIds(["ID_1", "ID_2"]); 45 | expect(results).toHaveLength(0); 46 | }); 47 | 48 | test("Gets Jargon metrics", async () => { 49 | MetricSearchCtl.fetchLatestMetrics = jest.fn().mockResolvedValue(["a"]); 50 | 51 | const wrapper = renderer.create(); 52 | const instance = wrapper.getInstance(); 53 | const results = await instance.queryMetrics(["ID_1", "ID_2"]); 54 | expect(results).toStrictEqual(["a", "a"]); 55 | }); 56 | 57 | test("Gets Paper IDs", async () => { 58 | PaperSearchCtl.fetchPapersIDs = jest.fn().mockResolvedValue(["b"]); 59 | 60 | const wrapper = renderer.create(); 61 | const instance = wrapper.getInstance(); 62 | const results = await instance.queryPaperIds(["ID_1", "ID_2"]); 63 | expect(results).toStrictEqual(["b", "b"]); 64 | }); 65 | 66 | test("Shows search HTML elements", () => { 67 | const { container } = render(); 68 | 69 | const searchIcons = container.getElementsByClassName("circular icon"); 70 | const searchInput = container.getElementsByClassName("input"); 71 | 72 | expect(searchIcons).toHaveLength(2); 73 | expect(searchInput).toHaveLength(2); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /tests/papers/components/panel/search/PaperSearch.test.js: -------------------------------------------------------------------------------- 1 | /* encoding: utf-8 */ 2 | 3 | import { render } from "@testing-library/react"; 4 | import renderer from "react-test-renderer"; 5 | import PaperSearch from "../../../../../src/scenes/papers/components/panel/search/PaperSearch"; 6 | import PaperSearchCtl from "../../../../../src/scenes/papers/controllers/paperscape/PaperSearch"; 7 | 8 | describe("PaperSearch component", () => { 9 | /** Set of tests for the PaperSearch component */ 10 | 11 | test("Updates search input", () => { 12 | const wrapper = renderer.create(); 13 | const instance = wrapper.getInstance(); 14 | const testValue = "example"; 15 | const testEvent = { target: { value: testValue } }; 16 | 17 | instance.state.paperSearch = null; 18 | instance.updateSearch(testEvent); 19 | 20 | const searchValue = instance.state.paperSearch; 21 | expect(searchValue).toBe(testValue); 22 | }); 23 | 24 | test("Updates search type", () => { 25 | const wrapper = renderer.create(); 26 | const instance = wrapper.getInstance(); 27 | const testValue = "example"; 28 | const testEvent = { value: testValue }; 29 | 30 | instance.state.paperSearchType = null; 31 | instance.updateSearchType(testEvent); 32 | 33 | const searchType = instance.state.paperSearchType; 34 | expect(searchType).toBe(testValue); 35 | }); 36 | 37 | test("Handles empty results", async () => { 38 | PaperSearchCtl.fetchPapersIDs = jest.fn().mockResolvedValue([]); 39 | 40 | const wrapper = renderer.create(); 41 | const instance = wrapper.getInstance(); 42 | const results = await instance.searchPapers(); 43 | expect(results).toBeNull(); 44 | }); 45 | 46 | test("Shows search HTML elements", () => { 47 | const { container, getByText } = render(); 48 | 49 | const searchLabel = getByText("Search:"); 50 | const searchDrop = container.getElementsByClassName("selection dropdown"); 51 | const searchIcons = container.getElementsByClassName("circular icon"); 52 | const searchInput = container.getElementsByClassName("input"); 53 | 54 | expect(searchLabel).toBeDefined(); 55 | expect(searchDrop).toHaveLength(1); 56 | expect(searchIcons).toHaveLength(1); 57 | expect(searchInput).toHaveLength(1); 58 | }); 59 | }); 60 | --------------------------------------------------------------------------------