From f17b9f3b6f7f9e678c681c719eea6fb5c41a387f Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Sat, 4 Feb 2023 14:12:12 +0100 Subject: [PATCH] basic map pins --- app/Http/Controllers/TechniqueController.php | 7 +++ app/Models/TechniqueMap.php | 21 +++++++ .../2023_02_01_153251_technique_map.php | 35 ++++++++++++ resources/js/components/common/Icon.js | 1 + resources/js/components/map/Buttons.js | 45 ++++----------- resources/js/components/map/OpenSeadragon.js | 36 +++++++++++- resources/js/components/map/Overlay.js | 37 +++++++++++++ resources/js/components/map/Pin.js | 55 +++++++++++++++++++ resources/js/components/map/Pins.js | 14 +++++ resources/js/components/map/Popover.js | 54 ++++++++++++++++++ resources/js/components/pages/Map.js | 2 + resources/sass/app.scss | 1 + resources/sass/map.scss | 18 ++++++ routes/api.php | 2 + 14 files changed, 294 insertions(+), 34 deletions(-) create mode 100644 app/Models/TechniqueMap.php create mode 100644 database/migrations/2023_02_01_153251_technique_map.php create mode 100644 resources/js/components/map/Overlay.js create mode 100644 resources/js/components/map/Pin.js create mode 100644 resources/js/components/map/Pins.js create mode 100644 resources/js/components/map/Popover.js create mode 100644 resources/sass/map.scss diff --git a/app/Http/Controllers/TechniqueController.php b/app/Http/Controllers/TechniqueController.php index 8694c04..b27465e 100644 --- a/app/Http/Controllers/TechniqueController.php +++ b/app/Http/Controllers/TechniqueController.php @@ -3,12 +3,19 @@ namespace App\Http\Controllers; use App\Models\Technique; +use App\Models\TechniqueMap; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; class TechniqueController extends Controller { + public function forMap($map) { + $techs = TechniqueMap::with(['technique', 'technique.relations'])->where('map', '=', $map); + + return $techs->get()->toJson(); + } + public function search(Request $request) { $validatedData = $request->validate([ 'phrase' => 'string|nullable', diff --git a/app/Models/TechniqueMap.php b/app/Models/TechniqueMap.php new file mode 100644 index 0000000..d19a005 --- /dev/null +++ b/app/Models/TechniqueMap.php @@ -0,0 +1,21 @@ +belongsTo(Technique::class); + } + + protected $hidden = [ + 'created_at', + 'updated_at', + ]; + +} diff --git a/database/migrations/2023_02_01_153251_technique_map.php b/database/migrations/2023_02_01_153251_technique_map.php new file mode 100644 index 0000000..42e9230 --- /dev/null +++ b/database/migrations/2023_02_01_153251_technique_map.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('technique_id')->constrained(); + $table->string('map'); + $table->double('x'); + $table->double('y'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('technique_maps'); + } +}; diff --git a/resources/js/components/common/Icon.js b/resources/js/components/common/Icon.js index bce75f5..c9d604a 100644 --- a/resources/js/components/common/Icon.js +++ b/resources/js/components/common/Icon.js @@ -73,6 +73,7 @@ Icon.LANGUAGE = makePreset('LanguageIcon', 'language'); Icon.LOCKED = makePreset('LockedIcon', 'lock'); Icon.LOGOUT = makePreset('LogoutIcon', 'sign-out-alt'); Icon.PENDING = makePreset('PendingIcon', 'clock'); +Icon.PIN = makePreset('PinIcon', 'location-pin'); Icon.PROTOCOL = makePreset('ProtocolIcon', 'file-alt'); Icon.REJECT = makePreset('RejectIcon', 'square-xmark'); Icon.REMOVE = makePreset('RemoveIcon', 'square-xmark'); diff --git a/resources/js/components/map/Buttons.js b/resources/js/components/map/Buttons.js index b43fc0e..fdffe92 100644 --- a/resources/js/components/map/Buttons.js +++ b/resources/js/components/map/Buttons.js @@ -5,42 +5,21 @@ import { useTranslation } from 'react-i18next'; import { useOpenSeadragon } from './OpenSeadragon'; const Buttons = () => { - const { viewer } = useOpenSeadragon(); + const { activeMap, setActiveMap } = useOpenSeadragon(); const { t } = useTranslation(); - const goToPage = React.useCallback((p) => { - if (viewer) viewer.goToPage(p); - }, [viewer]); - return
- - - - + {['lw', 'dw', 'sp', 'uw'].map(map => + + )}
; }; diff --git a/resources/js/components/map/OpenSeadragon.js b/resources/js/components/map/OpenSeadragon.js index f854318..b6158a5 100644 --- a/resources/js/components/map/OpenSeadragon.js +++ b/resources/js/components/map/OpenSeadragon.js @@ -1,3 +1,4 @@ +import axios from 'axios'; import OpenSeadragon from 'openseadragon'; import PropTypes from 'prop-types'; import React from 'react'; @@ -7,6 +8,8 @@ export const Context = React.createContext({}); export const useOpenSeadragon = () => React.useContext(Context); export const Provider = React.forwardRef(({ children }, ref) => { + const [activeMap, setActiveMap] = React.useState('lw'); + const [pins, setPins] = React.useState([]); const [viewer, setViewer] = React.useState(null); React.useEffect(() => { @@ -65,7 +68,38 @@ export const Provider = React.forwardRef(({ children }, ref) => { }; }, [ref.current]); - return + React.useEffect(() => { + if (!viewer) return; + switch (activeMap) { + case 'lw': + viewer.goToPage(0); + break; + case 'dw': + viewer.goToPage(1); + break; + case 'sp': + viewer.goToPage(2); + break; + case 'uw': + viewer.goToPage(3); + break; + } + const controller = new AbortController(); + axios.get(`/api/markers/${activeMap}`, { + signal: controller.signal, + }).then(response => { + setPins(response.data || []); + }).catch(e => { + if (!axios.isCancel(e)) { + console.error(e); + } + }); + return () => { + controller.abort(); + }; + }, [activeMap, viewer]); + + return {children} ; }); diff --git a/resources/js/components/map/Overlay.js b/resources/js/components/map/Overlay.js new file mode 100644 index 0000000..4068bea --- /dev/null +++ b/resources/js/components/map/Overlay.js @@ -0,0 +1,37 @@ +import OpenSeadragon from 'openseadragon'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { createPortal } from 'react-dom'; + +import { useOpenSeadragon } from './OpenSeadragon'; + +const Overlay = ({ children, onClick, x, y }) => { + const { viewer } = useOpenSeadragon(); + const [element] = React.useState(document.createElement('div')); + + React.useEffect(() => { + if (!viewer) return; + viewer.addOverlay( + element, + new OpenSeadragon.Point(x, y), + OpenSeadragon.Placement.CENTER, + ); + if (onClick) { + new OpenSeadragon.MouseTracker({ + element, + clickHandler: onClick, + }); + } + }, [onClick, viewer, x, y]); + + return createPortal(children, element); +}; + +Overlay.propTypes = { + children: PropTypes.node, + onClick: PropTypes.func, + x: PropTypes.number, + y: PropTypes.number, +}; + +export default Overlay; diff --git a/resources/js/components/map/Pin.js b/resources/js/components/map/Pin.js new file mode 100644 index 0000000..69b6d77 --- /dev/null +++ b/resources/js/components/map/Pin.js @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Link, useNavigate } from 'react-router-dom'; + +import Overlay from './Overlay'; +import Popover from './Popover'; +import Icon from '../common/Icon'; +import { getLink, getTranslation } from '../../helpers/Technique'; +import i18n from '../../i18n'; + +const Pin = ({ pin }) => { + const [showPopover, setShowPopover] = React.useState(false); + const ref = React.useRef(); + + const navigate = useNavigate(); + + const onClick = React.useCallback((e) => { + if (ref.current && ref.current.contains(e.originalTarget)) { + if (e.originalTarget.tagName === 'A') { + navigate(new URL(e.originalTarget.href).pathname); + } + } else { + if (pin.technique.type === 'location') { + setShowPopover(s => !s); + } else { + navigate(getLink(pin.technique)); + } + } + }, [pin]); + + return +
+ + + + {pin.technique.type === 'location' ? +
+ +
+ : null} +
+
; +}; + +Pin.propTypes = { + pin: PropTypes.shape({ + technique: PropTypes.shape({ + type: PropTypes.string, + }), + x: PropTypes.number, + y: PropTypes.number, + }), +}; + +export default Pin; diff --git a/resources/js/components/map/Pins.js b/resources/js/components/map/Pins.js new file mode 100644 index 0000000..8b37ee9 --- /dev/null +++ b/resources/js/components/map/Pins.js @@ -0,0 +1,14 @@ +import React from 'react'; + +import { useOpenSeadragon } from './OpenSeadragon'; +import Pin from './Pin'; + +const Pins = () => { + const { pins } = useOpenSeadragon(); + + return pins.map(pin => + + ); +}; + +export default Pins; diff --git a/resources/js/components/map/Popover.js b/resources/js/components/map/Popover.js new file mode 100644 index 0000000..02d4070 --- /dev/null +++ b/resources/js/components/map/Popover.js @@ -0,0 +1,54 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Card, ListGroup } from 'react-bootstrap'; +import { Link } from 'react-router-dom'; + +import { + getLink, + getRelations, + getTranslation, + hasRelations, + sorted, +} from '../../helpers/Technique'; +import i18n from '../../i18n'; + +const Popover = ({ show, technique }) => +
+ + + + {getTranslation(technique, 'title', i18n.language)} + + + {technique.short ? + + + {getTranslation(technique, 'short', i18n.language)} + + + : null} + {hasRelations(technique, 'related') ? + + {sorted(getRelations(technique, 'related')).map(r => + + + {getTranslation(r, 'title', i18n.language)} + + + )} + + : null} + +
; + +Popover.propTypes = { + show: PropTypes.bool, + technique: PropTypes.shape({ + short: PropTypes.string, + }), +}; + +export default Popover; diff --git a/resources/js/components/pages/Map.js b/resources/js/components/pages/Map.js index 50a28fb..d8a1039 100644 --- a/resources/js/components/pages/Map.js +++ b/resources/js/components/pages/Map.js @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import Buttons from '../map/Buttons'; import OpenSeadragon from '../map/OpenSeadragon'; +import Pins from '../map/Pins'; const Map = () => { const container = React.useRef(); @@ -16,6 +17,7 @@ const Map = () => {
+ ; }; diff --git a/resources/sass/app.scss b/resources/sass/app.scss index d050607..90af358 100644 --- a/resources/sass/app.scss +++ b/resources/sass/app.scss @@ -15,6 +15,7 @@ @import 'discord'; @import 'form'; @import 'front'; +@import 'map'; @import 'participants'; @import 'results'; @import 'rounds'; diff --git a/resources/sass/map.scss b/resources/sass/map.scss new file mode 100644 index 0000000..95d645a --- /dev/null +++ b/resources/sass/map.scss @@ -0,0 +1,18 @@ +.map-pin { + path { + fill: red; + stroke: black; + stroke-width: 2px; + vector-effect: non-scaling-stroke; + } +} + +.map-popover { + position: absolute; + width: 40vw; + min-width: 20em; + + &.hidden { + display: none; + } +} diff --git a/routes/api.php b/routes/api.php index 4ec1bb1..4c1d8ab 100644 --- a/routes/api.php +++ b/routes/api.php @@ -36,6 +36,8 @@ Route::get('discord-guilds', 'App\Http\Controllers\DiscordGuildController@search Route::get('discord-guilds/{guild_id}', 'App\Http\Controllers\DiscordGuildController@single'); Route::get('discord-guilds/{guild_id}/channels', 'App\Http\Controllers\DiscordChannelController@search'); +Route::get('markers/{map}', 'App\Http\Controllers\TechniqueController@forMap'); + Route::get('protocol/{tournament}', 'App\Http\Controllers\ProtocolController@forTournament'); Route::post('results', 'App\Http\Controllers\ResultController@create'); -- 2.39.2