Skip to content

Commit

Permalink
Merge pull request #2753 from serge-web/2743-new-mapping-channel
Browse files Browse the repository at this point in the history
2743: introduce new mapping channel
  • Loading branch information
IanMayo authored Jan 13, 2024
2 parents 54ae35f + d6d7b15 commit 3d8596b
Show file tree
Hide file tree
Showing 22 changed files with 410 additions and 84 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CHAT_MESSAGE, CREATE_EXPORT_ITEM, CUSTOM_MESSAGE, FEEDBACK_MESSAGE, INFO_MESSAGE, EXPORT_ITEM_MESSAGES, LOADER } from 'src/config'
import flatten from 'flat'
import { ExportItemsUiActionTypes, ExportItem, Wargame, Message, MessageInfoType, ExportItemData } from 'src/custom-types'
import { ExportItemsUiActionTypes, ExportItem, Wargame, Message, MessageInfoType, ExportItemData, MessageCustom } from 'src/custom-types'

interface ChannelTitles {
[property: string]: string
Expand Down Expand Up @@ -83,12 +83,12 @@ const exportDataGrouped = (messages: Message[], channelTitles: ChannelTitles): E
for (const message of messages) {
let msgType: string = message.messageType
if (message.messageType === CUSTOM_MESSAGE) {
msgType += ' ' + message.details.messageType
msgType += ' ' + (message as MessageCustom).details.messageType
}
if (msgType && !messageTypes[msgType]) {
messageTypes[msgType] = true
const rowsAndFields: ExportDataGroupedGetRowsAndFields = exportDataGroupedGetRowsAndFields(messages, message, channelTitles)
const title = message.messageType === CUSTOM_MESSAGE ? 'CM ' + message.details.messageType : msgType
const title = message.messageType === CUSTOM_MESSAGE ? 'CM ' + (message as MessageCustom).details.messageType : msgType
// The max length of the sheet name is 31
data.push({
title: title.length > 31
Expand All @@ -106,16 +106,16 @@ const exportDataGrouped = (messages: Message[], channelTitles: ChannelTitles): E

const exportDataGroupedGetRowsAndFields = (messages: Message[], message: Message, channelTitles: ChannelTitles) :ExportDataGroupedGetRowsAndFields => {
const messagesFiltered: Message[] = message.messageType === CUSTOM_MESSAGE
? messages.filter(msg => msg.messageType === CUSTOM_MESSAGE && message.details.messageType === msg.details.messageType)
? messages.filter(msg => msg.messageType === CUSTOM_MESSAGE && (message as MessageCustom).details.messageType === (msg as MessageCustom).details.messageType)
: messages.filter(({ messageType }) => message.messageType === messageType)

const fields: string[] = []
const rows: string[][] = []

const messagesWithChannelNames: FlatMessages[] = messagesFiltered.map(msg => {
if (msg.messageType === CUSTOM_MESSAGE || msg.messageType === CHAT_MESSAGE || msg.messageType === FEEDBACK_MESSAGE) {
if (msg.details.channel && channelTitles[msg.details.channel]) {
msg.details.channel = channelTitles[msg.details.channel]
if ((msg as MessageCustom).details.channel && channelTitles[(msg as MessageCustom).details.channel]) {
(msg as MessageCustom).details.channel = channelTitles[(msg as MessageCustom).details.channel]
}
}
const flatMsg: FlatMessage = keysSimplify(flatten<Message, FlatMessage>(msg))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable complexity */
import * as ActionConstant from 'src/config'
import copyState from '../../Helpers/copyStateHelper'
import * as wargamesHandlers from './helpers/wargamesHandlers'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as wargamesApi from '../../api/wargames_api'
import isError from '../../Helpers/isError'
import { addNotification } from '../Notification/Notification_ActionCreators'

import { ChatMessage, Message, MessageChannel, MessageCustom, MessageDetails, MessageDetailsFrom, MessageFeedback, MessageInfoType, PlayerUiActionTypes, Role, TemplateBodysByKey, Wargame } from 'src/custom-types'
import { ChatMessage, MappingMessage, Message, MessageChannel, MessageCustom, MessageDetails, MessageDetailsFrom, MessageFeedback, MessageInfoType, PlayerUiActionTypes, Role, TemplateBodysByKey, Wargame } from 'src/custom-types'

export const setCurrentWargame = (wargame: Wargame): PlayerUiActionTypes => ({
type: SET_CURRENT_WARGAME_PLAYER,
Expand Down Expand Up @@ -129,6 +129,10 @@ export const sendFeedbackMessage = (dbName: string, fromDetails: MessageDetailsF
}
}

export const sendMappingMessage = (dbName: string, message: MappingMessage): void => {
wargamesApi.postMappingMessage(dbName, message)
}

export const failedLoginFeedbackMessage = (dbName: string, password: string, turnNumber: number): Function => {
return async (): Promise<void> => {
const address = await wargamesApi.getIpAddress()
Expand Down
54 changes: 54 additions & 0 deletions client/src/Components/CoreMappingChannel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { useEffect, useState } from 'react'
import { ChannelMapping, MappingMessage, MessageChannel } from 'src/custom-types'
import {
getAllWargameMessages, sendMappingMessage
} from '../ActionsAndReducers/playerUi/playerUi_ActionCreators'

import 'src/themes/App.scss'
import { usePlayerUiDispatch, usePlayerUiState } from '../Store/PlayerUi'

import { Phase } from 'src/config'
import CoreMapping from './local/atoms/core-mapping'

const CoreMappingChannel: React.FC<{ channelId: string, isCustomChannel?: boolean }> = ({ channelId }) => {
const state = usePlayerUiState()
const playerUiDispatch = usePlayerUiDispatch()
const [channelTabClass, setChannelTabClass] = useState<string>('')
const { selectedRole, selectedForce } = state
const channelUI = state.channels[channelId]
// const selectedForceId = state.selectedForce ? state.selectedForce.uniqid : ''
if (selectedForce === undefined) throw new Error('selectedForce is undefined')

const channel = channelUI.cData as ChannelMapping
const messages = state.channels[channelId].messages as Array<MessageChannel>

useEffect(() => {
const channelClassName = state.channels[channelId].name.toLowerCase().replace(/ /g, '-')
if (state.channels[channelId].messages!.length === 0) {
getAllWargameMessages(state.currentWargame)(playerUiDispatch)
}
setChannelTabClass(`tab-content-${channelClassName}`)
}, [])

const messageHandler = (message: MappingMessage): void => {
sendMappingMessage(state.currentWargame, message)
}

return (
<div className={channelTabClass} data-channel-id={channelId}>
<CoreMapping
postBack={messageHandler}
channel={channel}
currentPhase={Phase.Planning}
currentTurn={1}
forces={[]}
messages={messages}
playerForce={selectedForce}
playerRole={selectedRole}
openPanelAsDefault={true}
/>
</div>
)
}

export default CoreMappingChannel
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@

/** code taken from here: https://github.com/gabzim/circle-to-polygon/blob/master/index.js */

import { Polygon, Position } from 'geojson'

const defaultEarthRadius = 6378137 // equatorial Earth radius

function toRadians (angleInDegrees: number): number {
return (angleInDegrees * Math.PI) / 180
}

function toDegrees (angleInRadians: number): number {
return (angleInRadians * 180) / Math.PI
}

function offset (c1: [number, number], distance: number, earthRadius: number, bearing: number): [number, number] {
const lat1: number = toRadians(c1[1])
const lon1: number = toRadians(c1[0])
const dByR: number = distance / earthRadius
const lat: number = Math.asin(
Math.sin(lat1) * Math.cos(dByR) + Math.cos(lat1) * Math.sin(dByR) * Math.cos(bearing)
)
const lon: number =
lon1 +
Math.atan2(
Math.sin(bearing) * Math.sin(dByR) * Math.cos(lat1),
Math.cos(dByR) - Math.sin(lat1) * Math.sin(lat)
)
return [toDegrees(lon), toDegrees(lat)]
}

export default function circleToPolygon (center: [number, number], radius: number, options: {
numberOfEdges?: number
earthRadius?: number
bearing?: number
rightHandRule?: boolean
}): Polygon {
const n: number = getNumberOfEdges(options)
const earthRadius: number = getEarthRadius(options)
const bearing: number = getBearing(options)
const direction: number = getDirection(options)

const start: number = toRadians(bearing)
const coordinates: Position[][] = [[]]
for (let i = 0; i < n; ++i) {
coordinates[0].push(
offset(
center, radius, earthRadius, start + (direction * 2 * Math.PI * -i) / n
)
)
}
coordinates[0].push(coordinates[0][0])

return {
type: 'Polygon',
coordinates: coordinates
}
}

function getNumberOfEdges (options: { numberOfEdges?: number }): number {
if (isUndefinedOrNull(options)) {
return 32
} else if (isObjectNotArray(options)) {
const numberOfEdges: number | undefined = options.numberOfEdges
return numberOfEdges === undefined ? 32 : numberOfEdges
}
return options as number
}

function getEarthRadius (options: { earthRadius?: number }): number {
if (isUndefinedOrNull(options)) {
return defaultEarthRadius
} else if (isObjectNotArray(options)) {
const earthRadius: number | undefined = options.earthRadius
return earthRadius === undefined ? defaultEarthRadius : earthRadius
}
return options as number
}

function getDirection (options: { rightHandRule?: boolean }): number {
if (isObjectNotArray(options) && options.rightHandRule) {
return -1
}
return 1
}

function getBearing (options: { bearing?: number }): number {
if (isUndefinedOrNull(options)) {
return 0
} else if (isObjectNotArray(options)) {
const bearing: number | undefined = options.bearing
return bearing === undefined ? 0 : bearing
}
return options as number
}

function isObjectNotArray (argument: any): boolean {
return argument !== null && typeof argument === 'object' && !Array.isArray(argument)
}

function isUndefinedOrNull (argument: any): boolean {
return argument === null || argument === undefined
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ const MapControls: React.FC<GeomanControlProps> = ({ onCreate }) => {
case 'Rectangle':
onCreate(e as unknown as PM.ChangeEventHandler)
break
case 'Circle':
onCreate(e as unknown as PM.ChangeEventHandler)
break
default:
console.log('OnCreate Unimplemented !!!', e['shape'])
}
Expand All @@ -91,7 +94,8 @@ const MapControls: React.FC<GeomanControlProps> = ({ onCreate }) => {
position: 'topright',
rotateMode: true,
pinningOption: true,
snappingOption: true
snappingOption: true,
drawCircleMarker: false
}}
globalOptions={{}}
onCreate={e => map.removeLayer(e.layer)}
Expand Down
26 changes: 22 additions & 4 deletions client/src/Components/local/atoms/core-mapping/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* global it expect */
import React from 'react'
import L from 'leaflet'
import renderer from 'react-test-renderer'
import CoreMapping from './index'
import { CHANNEL_MAPPING, Phase } from 'src/config'
import { ChannelMapping } from 'src/custom-types'
import { ChannelMapping, ForceData } from 'src/custom-types'
import { noop } from 'lodash'

jest.mock('react-leaflet-v4', () => ({
MapContainer: (): React.ReactElement => <></>,
Expand Down Expand Up @@ -32,12 +32,30 @@ const channel: ChannelMapping = {
}
}

const bounds = L.latLngBounds(L.latLng(51.405, -0.02), L.latLng(51.605, -0.13))
const playerForce: ForceData = {
color: '#000',
dirty: false,
iconURL: '',
name: '',
overview: '',
roles: [],
uniqid: 'f-red'
}

describe('Core Mapping component:', () => {
it('renders correctly', () => {
const tree = renderer
.create(<CoreMapping bounds={bounds} playerForce={'f-red'} messages={[]} channel={channel} playerRole={'mgr'} currentTurn={1} forces={[]} currentPhase={Phase.Planning}/>)
.create(<CoreMapping
playerForce={playerForce}
messages={[]}
channel={channel}
playerRole={'mgr'}
currentTurn={1}
forces={[]}
currentPhase={Phase.Planning}
postBack={noop}
openPanelAsDefault={false}
/>)
.toJSON()
expect(tree).toMatchSnapshot()
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import L from 'leaflet'
import CoreMapping from './index'
import docs from './README.md'
import { CHANNEL_MAPPING, CUSTOM_MESSAGE, MAPPING_MESSAGE, PARTICIPANT_MAPPING, Phase } from 'src/config'
import { ChannelMapping, MappingMessage, CoreProperties, CoreRenderer, EnumProperty, MilSymProperties, MilSymRenderer, NumberProperty, RENDERER_CORE, RENDERER_MILSYM } from 'src/custom-types'
import { ChannelMapping, MappingMessage, CoreProperties, CoreRenderer, EnumProperty, MilSymProperties, MilSymRenderer, NumberProperty, RENDERER_CORE, RENDERER_MILSYM, ForceData } from 'src/custom-types'
import { Feature, FeatureCollection } from 'geojson'
import { generateFeatures } from './helper/feature-generator'
import { noop } from 'lodash'

const wrapper: React.FC = (storyFn: any) => <div style={{ height: '600px', position: 'relative' }}>{storyFn()}</div>

Expand All @@ -23,7 +24,7 @@ export default {
}

const largeBounds = L.latLngBounds(L.latLng(45, -30), L.latLng(60, 30))
const bounds = L.latLngBounds(L.latLng(51.405, -0.02), L.latLng(51.605, -0.13))
// const bounds = L.latLngBounds(L.latLng(51.405, -0.02), L.latLng(51.605, -0.13))

/** PROPERTY DEFINITIONS */

Expand Down Expand Up @@ -268,14 +269,44 @@ const bulkMessage: MappingMessage = {

console.log(coreMessage)

const playerForce: ForceData = {
color: '#000',
dirty: false,
iconURL: '',
name: '',
overview: '',
roles: [],
uniqid: 'f-red'
}

export const Default: React.FC = () => {
return (
<CoreMapping bounds={bounds} playerForce={'f-red'} messages={[coreMessage]} channel={coreMapChannel} playerRole={'mgr'} currentTurn={1} forces={[]} currentPhase={Phase.Planning}/>
<CoreMapping
playerForce={playerForce}
messages={[coreMessage]}
channel={coreMapChannel}
playerRole={'mgr'}
currentTurn={1}
forces={[]}
currentPhase={Phase.Planning}
postBack={noop}
openPanelAsDefault={false}
/>
)
}

export const Bulk: React.FC = () => {
return (
<CoreMapping bounds={largeBounds} playerForce={'f-red'} messages={[bulkMessage]} channel={coreMapChannel} playerRole={'mgr'} currentTurn={1} forces={[]} currentPhase={Phase.Planning}/>
<CoreMapping
playerForce={playerForce}
messages={[bulkMessage]}
channel={coreMapChannel}
playerRole={'mgr'}
currentTurn={1}
forces={[]}
currentPhase={Phase.Planning}
postBack={noop}
openPanelAsDefault={false}
/>
)
}
Loading

0 comments on commit 3d8596b

Please sign in to comment.