diff --git a/.vscode/settings.json b/.vscode/settings.json index 97a35ac..b0ed334 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,10 +3,12 @@ "*.css": "tailwindcss" }, "cSpell.words": [ + "consts", "headlessui", "heroicons", "konva", "libretranslate", + "tailwindcss", "Tesseract", "Textualize", "wailsjs" diff --git a/core/ContextGroup/ContextGroupCollection.go b/core/ContextGroup/ContextGroupCollection.go new file mode 100644 index 0000000..dfd0417 --- /dev/null +++ b/core/ContextGroup/ContextGroupCollection.go @@ -0,0 +1,147 @@ +package contextGroup + +import ( + "fmt" + "textualize/entities" +) + +type ContextGroupCollection struct { + Groups []entities.LinkedAreaList +} + +var contextGroupCollectionInstance *ContextGroupCollection + +func GetContextGroupCollection() *ContextGroupCollection { + if contextGroupCollectionInstance == nil { + contextGroupCollectionInstance = &ContextGroupCollection{} + } + return contextGroupCollectionInstance +} + +func SetContextGroupCollection(collection ContextGroupCollection) *ContextGroupCollection { + contextGroupCollectionInstance = &collection + return contextGroupCollectionInstance +} + +func SetContextGroupCollectionBySerialized(serialized []entities.SerializedLinkedProcessedArea) *ContextGroupCollection { + newInstance := ContextGroupCollection{} + + newInstance.Groups = append(newInstance.Groups, entities.DeserializeLinkedAreaList(serialized)) + + SetContextGroupCollection(newInstance) + return &newInstance +} + +func (collection *ContextGroupCollection) DoesGroupExistBetweenProcessedAreas(ancestorAreaId string, descendantAreaId string) bool { + ancestorGroup, _ := collection.FindGroupByLinkedProcessedAreaId(ancestorAreaId) + descendantGroup, _ := collection.FindGroupByLinkedProcessedAreaId(descendantAreaId) + + isAncestorInAnyInGroup := ancestorGroup != nil + isDescendantInAnyInGroup := descendantGroup != nil + areBothInAnyInGroup := isAncestorInAnyInGroup && isDescendantInAnyInGroup + areBothInSameGroup := false + if areBothInAnyInGroup { + areBothInSameGroup = ancestorGroup.Id == descendantGroup.Id + } + + return areBothInSameGroup +} + +func (collection *ContextGroupCollection) DisconnectProcessedAreas(ancestorAreaId string, descendantAreaId string) bool { + doesConnectionExist := collection.DoesGroupExistBetweenProcessedAreas(ancestorAreaId, descendantAreaId) + + if !doesConnectionExist { + return false + } + + ancestorGroup, _ := collection.FindGroupByLinkedProcessedAreaId(ancestorAreaId) + + wasRemoved := false + for i, group := range collection.Groups { + if group.Id == ancestorGroup.Id { + collection.Groups = append(collection.Groups[:i], collection.Groups[i+1:]...) + wasRemoved = true + break + } + } + return wasRemoved +} + +func (collection *ContextGroupCollection) FindGroupById(id string) (*entities.LinkedAreaList, error) { + found := false + var foundGroup *entities.LinkedAreaList = nil + for _, group := range collection.Groups { + if group.Id == id { + found = true + foundGroup = &group + } + } + if !found { + return nil, fmt.Errorf("ContextGroupCollection.FindGroupById: Group with id %s not found", id) + } + return foundGroup, nil +} + +func (collection *ContextGroupCollection) FindGroupByLinkedProcessedAreaId(id string) (*entities.LinkedAreaList, error) { + found := false + var foundGroup *entities.LinkedAreaList = nil + for _, group := range collection.Groups { + for n := group.First(); n != nil && !found; n = n.GetNext() { + if n.Area.Id == id { + found = true + foundGroup = &group + } + } + } + if !found { + return nil, fmt.Errorf("ContextGroupCollection.FindGroupByLinkedProcessedAreaId: Group with LinkedProcessedArea.Id %s not found", id) + } + return foundGroup, nil +} + +func (collection *ContextGroupCollection) ConnectProcessedAreas(ancestorNode entities.ProcessedArea, descendantNode entities.ProcessedArea) bool { + ancestorGroup, _ := collection.FindGroupByLinkedProcessedAreaId(ancestorNode.Id) + descendantGroup, _ := collection.FindGroupByLinkedProcessedAreaId(descendantNode.Id) + + isAncestorInAnyInGroup := ancestorGroup != nil + isDescendantInAnyInGroup := descendantGroup != nil + isEitherInAnyInGroup := isAncestorInAnyInGroup || isDescendantInAnyInGroup + areBothInAnyInGroup := isAncestorInAnyInGroup && isDescendantInAnyInGroup + areBothInSameGroup := false + if areBothInAnyInGroup { + areBothInSameGroup = ancestorGroup.Id == descendantGroup.Id + } + + if areBothInSameGroup { + return true + } + + if !isEitherInAnyInGroup { + collection.createNewGroupAndConnectNodes(ancestorNode, descendantNode) + return true + } + + if isAncestorInAnyInGroup && !isDescendantInAnyInGroup { + ancestorGroup.InsertAfter(ancestorNode.Id, descendantNode) + return true + } + + if !isAncestorInAnyInGroup && isDescendantInAnyInGroup { + descendantGroup.InsertBefore(descendantNode.Id, ancestorNode) + return true + } + + return false +} + +func (collection *ContextGroupCollection) createNewGroupAndConnectNodes(ancestorNode entities.ProcessedArea, descendantNode entities.ProcessedArea) { + newGroup := entities.LinkedAreaList{ + Id: ancestorNode.Id, + DocumentId: ancestorNode.DocumentId, + Head: &entities.LinkedProcessedArea{Area: ancestorNode}, + Tail: &entities.LinkedProcessedArea{Area: descendantNode}, + } + newGroup.Head.Next = newGroup.Tail + newGroup.Tail.Previous = newGroup.Head + collection.Groups = append(collection.Groups, newGroup) +} diff --git a/entities/ContextGroup.go b/entities/ContextGroup.go index 1526091..a103af9 100644 --- a/entities/ContextGroup.go +++ b/entities/ContextGroup.go @@ -3,8 +3,6 @@ package entities import ( "errors" "fmt" - - "github.com/google/uuid" ) type IndependentTranslatedWord struct { @@ -15,43 +13,57 @@ type IndependentTranslatedWord struct { type LinkedProcessedArea struct { Area ProcessedArea - previous *LinkedProcessedArea - next *LinkedProcessedArea + Previous *LinkedProcessedArea + Next *LinkedProcessedArea +} + +type SerializedLinkedProcessedArea struct { + AreaId string `json:"areaId"` + PreviousId string `json:"previousId"` + NextId string `json:"nextId"` +} + +type ContextGroupCollection struct { + Groups []LinkedAreaList } type LinkedAreaList struct { - head *LinkedProcessedArea - tail *LinkedProcessedArea + Id string + DocumentId string + TranslationText string + Head *LinkedProcessedArea + Tail *LinkedProcessedArea } func (l *LinkedAreaList) First() *LinkedProcessedArea { - return l.head + return l.Head } -func (linkedProcessedWord *LinkedProcessedArea) Next() *LinkedProcessedArea { - return linkedProcessedWord.next +func (linkedProcessedWord *LinkedProcessedArea) GetNext() *LinkedProcessedArea { + return linkedProcessedWord.Next } -func (linkedProcessedWord *LinkedProcessedArea) Prev() *LinkedProcessedArea { - return linkedProcessedWord.previous +func (linkedProcessedWord *LinkedProcessedArea) GetPrevious() *LinkedProcessedArea { + return linkedProcessedWord.Previous } // Create new node with value func (l *LinkedAreaList) Push(processedArea ProcessedArea) *LinkedAreaList { n := &LinkedProcessedArea{Area: processedArea} - if l.head == nil { - l.head = n // First node + if l.Head == nil { + l.Head = n // First node } else { - l.tail.next = n // Add after prev last node - n.previous = l.tail // Link back to prev last node + l.Tail.Next = n // Add after prev last node + n.Previous = l.Tail // Link back to prev last node } - l.tail = n // reset tail to newly added node + l.Tail = n // reset tail to newly added node return l } + func (l *LinkedAreaList) Find(id string) *LinkedProcessedArea { found := false var ret *LinkedProcessedArea = nil - for n := l.First(); n != nil && !found; n = n.Next() { + for n := l.First(); n != nil && !found; n = n.GetNext() { if n.Area.Id == id { found = true ret = n @@ -59,16 +71,17 @@ func (l *LinkedAreaList) Find(id string) *LinkedProcessedArea { } return ret } + func (l *LinkedAreaList) Delete(id string) bool { success := false node2del := l.Find(id) if node2del != nil { fmt.Println("Delete - FOUND: ", id) - prev_node := node2del.previous - next_node := node2del.next + prev_node := node2del.Previous + next_node := node2del.Next // Remove this node - prev_node.next = node2del.next - next_node.previous = node2del.previous + prev_node.Next = node2del.Next + next_node.Previous = node2del.Previous success = true } return success @@ -78,86 +91,83 @@ var errEmpty = errors.New("ERROR - List is empty") // Pop last item from list func (l *LinkedAreaList) Pop() (processedArea ProcessedArea, err error) { - if l.tail == nil { + if l.Tail == nil { err = errEmpty } else { - processedArea = l.tail.Area - l.tail = l.tail.previous - if l.tail == nil { - l.head = nil + processedArea = l.Tail.Area + l.Tail = l.Tail.Previous + if l.Tail == nil { + l.Head = nil } } return processedArea, err } -type ContextGroup struct { // TODO: possibly remove this and expand the LinkedAreaList struct instead - Id string - DocumentId string - LinkedAreaList LinkedAreaList - TranslationText string -} - -type ContextGroupCollection struct { // TODO: these methods should live in core not entitites - Groups []ContextGroup -} - -var contextGroupCollectionInstance *ContextGroupCollection - -func GetContextGroupCollection() *ContextGroupCollection { - if contextGroupCollectionInstance == nil { - contextGroupCollectionInstance = &ContextGroupCollection{} - } - - return contextGroupCollectionInstance -} - -func SetContextGroupCollection(collection ContextGroupCollection) *ContextGroupCollection { - contextGroupCollectionInstance = &collection - return contextGroupCollectionInstance -} - -func (collection *ContextGroupCollection) FindContextGroupByNodeId(id string) *ContextGroup { - var foundContextGroup *ContextGroup - for i, g := range collection.Groups { - if g.LinkedAreaList.Find(id) != nil { - foundContextGroup = &collection.Groups[i] - break +func (l *LinkedAreaList) InsertAfter(id string, processedArea ProcessedArea) bool { + found := false + for n := l.First(); n != nil && !found; n = n.GetNext() { + if n.Area.Id == id { + found = true + newNode := &LinkedProcessedArea{Area: processedArea} + newNode.Next = n.Next + newNode.Previous = n + n.Next = newNode } } - - return foundContextGroup + return found } -func (collection *ContextGroupCollection) CreateContextGroupFromProcessedArea(area ProcessedArea) bool { - fmt.Println("CreateContextGroupFromProcessedArea") - - newLinkedAreaList := LinkedAreaList{} - newLinkedAreaList.Push(area) - - newContextGroup := ContextGroup{ - Id: uuid.NewString(), - DocumentId: area.DocumentId, - LinkedAreaList: newLinkedAreaList, +func (l *LinkedAreaList) InsertBefore(id string, processedArea ProcessedArea) bool { + found := false + for n := l.First(); n != nil && !found; n = n.GetNext() { + if n.Area.Id == id { + found = true + newNode := &LinkedProcessedArea{Area: processedArea} + newNode.Next = n + newNode.Previous = n.Previous + n.Previous = newNode + } } - - collection.Groups = append(collection.Groups, newContextGroup) - return true + return found } -// TODO: completely rework this linked list and the collection -func (collection *ContextGroupCollection) ConnectAreaAsTailToNode(tailArea ProcessedArea, headArea ProcessedArea) bool { - headNodeContextGroup := collection.FindContextGroupByNodeId(headArea.Id) +func (l *LinkedAreaList) Serialize() []SerializedLinkedProcessedArea { + var serialized []SerializedLinkedProcessedArea + for n := l.First(); n != nil; n = n.GetNext() { + areaId := n.Area.Id + previousId := "" + if n.Previous != nil { + previousId = n.Previous.Area.Id + } + nextId := "" + if n.Next != nil { + nextId = n.Next.Area.Id + } - if headNodeContextGroup == nil { - collection.CreateContextGroupFromProcessedArea(headArea) - headNodeContextGroup = collection.FindContextGroupByNodeId(headArea.Id) + serialized = append(serialized, SerializedLinkedProcessedArea{ + AreaId: areaId, + PreviousId: previousId, + NextId: nextId, + }) } - - headNode := headNodeContextGroup.LinkedAreaList.Find(headArea.Id) - headNode.next = &LinkedProcessedArea{ - Area: tailArea, - previous: headNode, - } - - return true + return serialized +} + +func DeserializeLinkedAreaList(serialized []SerializedLinkedProcessedArea) LinkedAreaList { + linkedAreaList := LinkedAreaList{} + for _, serializedLinkedProcessedArea := range serialized { + linkedAreaList.Push(ProcessedArea{ + Id: serializedLinkedProcessedArea.AreaId, + }) + } + for _, serializedLinkedProcessedArea := range serialized { + linkedProcessedArea := linkedAreaList.Find(serializedLinkedProcessedArea.AreaId) + if serializedLinkedProcessedArea.PreviousId != "" { + linkedProcessedArea.Previous = linkedAreaList.Find(serializedLinkedProcessedArea.PreviousId) + } + if serializedLinkedProcessedArea.NextId != "" { + linkedProcessedArea.Next = linkedAreaList.Find(serializedLinkedProcessedArea.NextId) + } + } + return linkedAreaList } diff --git a/frontend/components/DocumentCanvas/Area.tsx b/frontend/components/DocumentCanvas/Area.tsx index 31a7e17..86551ba 100644 --- a/frontend/components/DocumentCanvas/Area.tsx +++ b/frontend/components/DocumentCanvas/Area.tsx @@ -12,39 +12,22 @@ import { useStage } from './context/provider' type Props = { isActive: boolean, area: entities.Area, - setHoveredOverAreaIds: Function - setHoveredProcessedArea: Function } type coordinates = { x: number, y: number } const Area = (props: Props) => { - const { getProcessedAreaById, selectedAreaId, setSelectedAreaId } = useProject() + const { selectedAreaId, setSelectedAreaId } = useProject() const { scale } = useStage() const shapeRef = React.useRef(null) const [isAreaContextMenuOpen, setIsAreaContextMenuOpen] = useState(false) const [areaContextMenuPosition, setAreaContextMenuPosition] = useState() - const { area, isActive, setHoveredOverAreaIds, setHoveredProcessedArea } = props + const { area, isActive } = props const a = area const width = (a.endX - a.startX) const height = (a.endY - a.startY) - const handleEnterOrLeave = (e: KonvaEventObject) => { - const stage = e.currentTarget.getStage()! - const currentMousePosition = stage.pointerPos - const intersectingNodes = stage.getAllIntersections(currentMousePosition) - const drawnAreas = intersectingNodes.filter(n => n.attrs?.isArea) - const drawnAreasIds = drawnAreas.map(d => d.attrs?.id) - setHoveredOverAreaIds(drawnAreasIds) - - const processedAreaRequests = drawnAreasIds.map(a => getProcessedAreaById(a || '')) - Promise.all(processedAreaRequests).then(responses => { - const validResponses = responses.filter(r => r?.id) as entities.ProcessedArea[] - setHoveredProcessedArea(validResponses || []) - }) - } - const handleContextMenu = (e: KonvaEventObject) => { e.evt.preventDefault() const stage = e.currentTarget.getStage() @@ -76,8 +59,6 @@ const Area = (props: Props) => { strokeWidth={1} strokeScaleEnabled={false} shadowForStrokeEnabled={false} - onMouseEnter={handleEnterOrLeave} - onMouseLeave={handleEnterOrLeave} onClick={() => handleAreaClick(a.id)} onContextMenu={handleContextMenu} isArea diff --git a/frontend/components/DocumentCanvas/Areas.tsx b/frontend/components/DocumentCanvas/Areas.tsx index 39182af..2341711 100644 --- a/frontend/components/DocumentCanvas/Areas.tsx +++ b/frontend/components/DocumentCanvas/Areas.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { Group } from 'react-konva' import { useProject } from '../../context/Project/provider' import { entities } from '../../wailsjs/wailsjs/go/models' @@ -12,12 +12,24 @@ import { useStage } from './context/provider' type Props = { scale: number } const Areas = ({ scale }: Props) => { - const { getSelectedDocument, selectedAreaId } = useProject() + const { getSelectedDocument, selectedAreaId, getProcessedAreaById } = useProject() const { isProcessedWordsVisible } = useStage() const areas = getSelectedDocument()?.areas || [] - const [hoveredOverAreaIds, setHoveredOverAreaIds] = useState([]) - const [hoveredProcessedAreas, setHoveredProcessedArea] = useState([]) const [editingWord, setEditingWord] = useState(null) + const [selectedProcessedArea, setSelectedProcessedArea] = useState(null) + + useEffect(() => { + if (!selectedAreaId) return setSelectedProcessedArea(null) + else { + getProcessedAreaById(selectedAreaId).then(res => { + if (res) setSelectedProcessedArea(res) + }).catch(err => { + console.warn('getProcessedAreaById', err) + setSelectedProcessedArea(null) + }) + } + + }, [selectedAreaId]) const renderEditingWord = () => { if (!editingWord) return @@ -29,26 +41,20 @@ const Areas = ({ scale }: Props) => { } const renderProcessedWords = () => { - if (!hoveredProcessedAreas.length) return + if (!selectedProcessedArea) return <> - return hoveredProcessedAreas.map(a => { - const words = a.lines.map(l => l.words).flat() + const words = selectedProcessedArea.lines.map(l => l.words).flat() return words.map((w, index) => ) - }) } const renderAreas = (areas: entities.Area[]) => areas.map((a, index) => { - return + return }) return diff --git a/frontend/components/DocumentCanvas/CanvasStage.tsx b/frontend/components/DocumentCanvas/CanvasStage.tsx index f0ef2e7..0be7910 100644 --- a/frontend/components/DocumentCanvas/CanvasStage.tsx +++ b/frontend/components/DocumentCanvas/CanvasStage.tsx @@ -9,15 +9,15 @@ import useImage from 'use-image' import { RectangleCoordinates } from './types' import DrawingArea from './DrawingArea' import getNormalizedRectToBounds from '../../utils/getNormalizedRectToBounds' -import processImageArea from '../../useCases/processImageArea' import { useStage } from './context/provider' import ContextConnections from './ContextConnections' +import processImageRect from '../../useCases/processImageRect' let downClickX: number let downClickY: number const CanvasStage = () => { - const { getSelectedDocument, requestAddArea, setSelectedAreaId } = useProject() + const { getSelectedDocument, updateDocuments, setSelectedAreaId } = useProject() const { scale, scaleStep, maxScale, size, setScale, isAreasVisible, isLinkAreaContextsVisible, isDrawingArea, setIsDrawingArea, startingContextConnection, setStartingContextConnection } = useStage() const [documentImage] = useImage(getSelectedDocument()?.path || '') const documentRef = useRef(null) @@ -55,11 +55,12 @@ const CanvasStage = () => { const normalizedDrawnRect = getNormalizedRectToBounds(drawingAreaRect, documentWidth, documentHeight, scale) const selectedDocumentId = getSelectedDocument()!.id - requestAddArea(selectedDocumentId, normalizedDrawnRect).then(addedArea => { - setSelectedAreaId(addedArea.id) - processImageArea(selectedDocumentId, addedArea.id) + processImageRect(selectedDocumentId, normalizedDrawnRect).then(async addedAreas => { + updateDocuments().then(response => { + if (!addedAreas.length) return + setSelectedAreaId(addedAreas[0].id) + }) }) - setDrawingAreaRect(null) } diff --git a/frontend/components/DocumentCanvas/ContextConnections/ConnectionLines.tsx b/frontend/components/DocumentCanvas/ContextConnections/ConnectionLines.tsx new file mode 100644 index 0000000..c86ae84 --- /dev/null +++ b/frontend/components/DocumentCanvas/ContextConnections/ConnectionLines.tsx @@ -0,0 +1,72 @@ +'use client' + +import React from 'react' +import { Group, Line } from 'react-konva' +import { useProject } from '../../../context/Project/provider' +import { useStage } from '../context/provider' + +const ConnectionLines = () => { + const { scale } = useStage() + const { getSelectedDocument, contextGroups } = useProject() + const areas = getSelectedDocument()?.areas || [] + + const renderLines = () => { + console.log('contextGroups', contextGroups) + if (!contextGroups?.length) return <> + + const linesAlreadyRendered = new Set() + const lines = contextGroups.map((contextGroup) => { + const currentArea = areas.find(a => a.id === contextGroup.areaId) + const nextArea = areas.find(a => a.id === contextGroup.nextId) + if (!currentArea || !nextArea) return + + if (linesAlreadyRendered.has(`${contextGroup.areaId}-${contextGroup.nextId}`)) return + if (linesAlreadyRendered.has(`${contextGroup.nextId}-${contextGroup.areaId}`)) return + + const startingPoint = { + x: ((currentArea.startX + currentArea.endX) * scale) / 2, + y: currentArea.startY * scale + } + + const startingTensionPoint = { + x: (startingPoint.x + (nextArea.startX * scale)) / 2, + y: startingPoint.y, + } + + const endingPoint = { + x: ((nextArea.startX + nextArea.endX) * scale) / 2, + y: nextArea.endY * scale + } + + const endingTensionPoint = { + x: (startingPoint.x + (nextArea.startX * scale)) / 2, + y: endingPoint.y, + } + + linesAlreadyRendered.add(`${contextGroup.areaId}-${contextGroup.nextId}`) + linesAlreadyRendered.add(`${contextGroup.nextId}-${contextGroup.areaId}`) + + return + }) + return lines.filter(l => !!l) + } + + return {renderLines()} +} + +export default ConnectionLines diff --git a/frontend/components/DocumentCanvas/ContextConnections/ConnectionPoints.tsx b/frontend/components/DocumentCanvas/ContextConnections/ConnectionPoints.tsx index 45de5a4..ae8415d 100644 --- a/frontend/components/DocumentCanvas/ContextConnections/ConnectionPoints.tsx +++ b/frontend/components/DocumentCanvas/ContextConnections/ConnectionPoints.tsx @@ -4,14 +4,14 @@ import { Circle, Group } from 'react-konva' import { useStage } from '../context/provider' import { entities } from '../../../wailsjs/wailsjs/go/models' import { KonvaEventObject } from 'konva/lib/Node' -import { RequestConnectAreaAsTailToNode } from '../../../wailsjs/wailsjs/go/ipc/Channel' +import { useProject } from '../../../context/Project/provider' type Props = { areas: entities.Area[] } const ConnectionPoints = (props: Props) => { const { isLinkAreaContextsVisible, scale, startingContextConnection, setStartingContextConnection } = useStage() + const { requestConnectProcessedAreas } = useProject() - - const handleContextAreaMouseDown = (e: KonvaEventObject) => { + const handleContextAreaMouseDown = async (e: KonvaEventObject) => { e.cancelBubble = true const clickedConnectionPoint = { isHead: e.currentTarget.attrs.isHead, @@ -24,10 +24,15 @@ const ConnectionPoints = (props: Props) => { || clickedConnectionPoint.areaId === startingContextConnection.areaId) return setStartingContextConnection(null) - console.log('connected points', startingContextConnection, clickedConnectionPoint) - const headId = clickedConnectionPoint.isHead ? clickedConnectionPoint.areaId : startingContextConnection.areaId - const tailId = !clickedConnectionPoint.isHead ? startingContextConnection.areaId : clickedConnectionPoint.areaId - RequestConnectAreaAsTailToNode(headId, tailId).then(res => console.log(res)).catch(err => console.warn(err)) + const headId = startingContextConnection.isHead ? startingContextConnection.areaId : clickedConnectionPoint.areaId + const tailId = !startingContextConnection.isHead ? startingContextConnection.areaId : clickedConnectionPoint.areaId + setStartingContextConnection(null) + + try { + await requestConnectProcessedAreas(headId, tailId) + } catch (err) { + console.warn('RequestConnectProcessedAreas', err) + } } const renderConnectingPointsForArea = (a: entities.Area) => { @@ -36,7 +41,7 @@ const ConnectionPoints = (props: Props) => { const headConnector = { />) } - return + return {connectorsToRender} } diff --git a/frontend/components/DocumentCanvas/ContextConnections/CurrentDrawingConnection.tsx b/frontend/components/DocumentCanvas/ContextConnections/CurrentDrawingConnection.tsx index 731e70b..550c727 100644 --- a/frontend/components/DocumentCanvas/ContextConnections/CurrentDrawingConnection.tsx +++ b/frontend/components/DocumentCanvas/ContextConnections/CurrentDrawingConnection.tsx @@ -2,18 +2,19 @@ import React from 'react' import { Line } from 'react-konva' -import { StartingContextConnection } from '../context/types' -import { entities } from '../../../wailsjs/wailsjs/go/models' import { Coordinates } from '../types' +import { useStage } from '../context/provider' +import { useProject } from '../../../context/Project/provider' type CurrentDrawingConnectionProps = { - startingContextConnection: StartingContextConnection | null - areas: entities.Area[], - scale: number, endDrawingPosition: Coordinates | null } const CurrentDrawingConnection = (props: CurrentDrawingConnectionProps) => { - const { startingContextConnection, areas, scale, endDrawingPosition } = props + const { endDrawingPosition } = props + const { startingContextConnection, scale } = useStage() + const { getSelectedDocument } = useProject() + const areas = getSelectedDocument()?.areas || [] + if (!startingContextConnection || !endDrawingPosition) return <> const { areaId, isHead } = startingContextConnection @@ -49,6 +50,7 @@ const CurrentDrawingConnection = (props: CurrentDrawingConnectionProps) => { strokeScaleEnabled={false} shadowForStrokeEnabled={false} tension={0.2} + listening={false} /> } diff --git a/frontend/components/DocumentCanvas/ContextConnections/index.tsx b/frontend/components/DocumentCanvas/ContextConnections/index.tsx index cfeb424..3d9320d 100644 --- a/frontend/components/DocumentCanvas/ContextConnections/index.tsx +++ b/frontend/components/DocumentCanvas/ContextConnections/index.tsx @@ -8,10 +8,11 @@ import Konva from 'konva' import { Coordinates } from '../types' import CurrentDrawingConnection from './CurrentDrawingConnection' import ConnectionPoints from './ConnectionPoints' +import ConnectionLines from './ConnectionLines' const ContextConnections = () => { const { getSelectedDocument } = useProject() - const { isLinkAreaContextsVisible, startingContextConnection, setStartingContextConnection, scale } = useStage() + const { isLinkAreaContextsVisible, startingContextConnection, scale } = useStage() const areas = getSelectedDocument()?.areas || [] const [endDrawingPosition, setEndDrawingPosition] = useState(null) @@ -34,7 +35,8 @@ const ContextConnections = () => { return - + + } diff --git a/frontend/components/DocumentCanvas/ToolingOverlay/ToolToggleButton.tsx b/frontend/components/DocumentCanvas/ToolingOverlay/ToolToggleButton.tsx index d1f552d..08672a2 100644 --- a/frontend/components/DocumentCanvas/ToolingOverlay/ToolToggleButton.tsx +++ b/frontend/components/DocumentCanvas/ToolingOverlay/ToolToggleButton.tsx @@ -9,7 +9,7 @@ type Icon = (props: React.SVGProps & { }) => JSX.Element const ToolToggleButton = (props: { icon: Icon, hint: string, isActive: boolean, onClick?: React.MouseEventHandler }) => { - return
+ return
diff --git a/frontend/components/workspace/Sidebar/AreaLineItem.tsx b/frontend/components/workspace/Sidebar/AreaLineItem.tsx index 309f00b..efa7c3c 100644 --- a/frontend/components/workspace/Sidebar/AreaLineItem.tsx +++ b/frontend/components/workspace/Sidebar/AreaLineItem.tsx @@ -15,13 +15,13 @@ const AreaLineItem = (props: { area: SidebarArea, documentId: string, index: num getAreaById, requestUpdateArea, setSelectedDocumentId, - setSelectedAreaId, requestChangeAreaOrder, requestDeleteAreaById, + selectedAreaId, + setSelectedAreaId, } = useProject() const { - selectedAreaId, isEditAreaNameInputShowing, setIsEditAreaNameInputShowing, dragOverAreaId, diff --git a/frontend/consts/index.ts b/frontend/consts/index.ts new file mode 100644 index 0000000..5325b58 --- /dev/null +++ b/frontend/consts/index.ts @@ -0,0 +1,7 @@ +const colors = { + BRAND_PRIMARY: { + hex: '#dc8dec', + } +} + +export { colors } diff --git a/frontend/context/Project/createContextGroupProviderMethods.ts b/frontend/context/Project/createContextGroupProviderMethods.ts new file mode 100644 index 0000000..d5d0045 --- /dev/null +++ b/frontend/context/Project/createContextGroupProviderMethods.ts @@ -0,0 +1,38 @@ +import { saveContextGroups } from '../../useCases/saveData' +import { RequestConnectProcessedAreas, GetSerializedContextGroups } from '../../wailsjs/wailsjs/go/ipc/Channel' +import { entities } from '../../wailsjs/wailsjs/go/models' + + +type Dependencies = { updateDocuments: Function } + +const createContextGroupProviderMethods = (dependencies?: Dependencies) => { + + const requestConnectProcessedAreas = async (headId: string, tailId: string) => { + let wasSuccessful = false + try { + wasSuccessful = await RequestConnectProcessedAreas(headId, tailId) + await saveContextGroups() + } catch (err) { + console.error(err) + } + dependencies?.updateDocuments() + return wasSuccessful + } + + const getSerializedContextGroups = async () => { + let response: entities.SerializedLinkedProcessedArea[] = [] + try { + response = await GetSerializedContextGroups() + } catch (err) { + console.error(err) + } + return response + } + + return { + requestConnectProcessedAreas, + getSerializedContextGroups, + } +} + +export default createContextGroupProviderMethods diff --git a/frontend/context/Project/createUserMarkdownProviderMethods.ts b/frontend/context/Project/createUserMarkdownProviderMethods.ts index c77fb19..b601e9d 100644 --- a/frontend/context/Project/createUserMarkdownProviderMethods.ts +++ b/frontend/context/Project/createUserMarkdownProviderMethods.ts @@ -1,6 +1,6 @@ import { saveUserProcessedMarkdown } from '../../useCases/saveData' import { GetUserMarkdownByDocumentId, RequestUpdateDocumentUserMarkdown } from '../../wailsjs/wailsjs/go/ipc/Channel' -import { ipc, entities } from '../../wailsjs/wailsjs/go/models' +import { entities } from '../../wailsjs/wailsjs/go/models' type Dependencies = {} diff --git a/frontend/context/Project/makeDefaultProject.ts b/frontend/context/Project/makeDefaultProject.ts index 4e2cb7e..9295646 100644 --- a/frontend/context/Project/makeDefaultProject.ts +++ b/frontend/context/Project/makeDefaultProject.ts @@ -1,10 +1,11 @@ -import { entities } from '../../wailsjs/wailsjs/go/models' +import { entities, ipc } from '../../wailsjs/wailsjs/go/models' import { ProjectContextType, UserProps } from './types' const makeDefaultProject = (): ProjectContextType => ({ id: '', documents: [] as entities.Document[], groups: [] as entities.Group[], + contextGroups: [] as entities.SerializedLinkedProcessedArea[], selectedAreaId: '', selectedDocumentId: '', getSelectedDocument: () => new entities.Document(), @@ -33,6 +34,9 @@ const makeDefaultProject = (): ProjectContextType => ({ requestUpdateProcessedWordById: (wordId, newTestValue) => Promise.resolve(false), getProcessedAreaById: (areaId) => Promise.resolve(undefined), requestUpdateProcessedArea: updatedProcessedArea => Promise.resolve(false), + requestConnectProcessedAreas: (headId, tailId) => Promise.resolve(false), + getSerializedContextGroups: () => Promise.resolve([]), + updateDocuments: () => Promise.resolve(new ipc.GetDocumentsResponse()) }) export default makeDefaultProject diff --git a/frontend/context/Project/provider.tsx b/frontend/context/Project/provider.tsx index 6c678e4..edb4970 100644 --- a/frontend/context/Project/provider.tsx +++ b/frontend/context/Project/provider.tsx @@ -10,6 +10,7 @@ import createAreaProviderMethods from './createAreaProviderMethods' import createDocumentProviderMethods from './createDocumentMethods' import createSessionProviderMethods from './createSessionProviderMethods' import createUserMarkdownProviderMethods from './createUserMarkdownProviderMethods' +import createContextGroupProviderMethods from './createContextGroupProviderMethods' const ProjectContext = createContext(makeDefaultProject()) @@ -21,15 +22,17 @@ type Props = { children: ReactNode, projectProps: ProjectProps } export function ProjectProvider({ children, projectProps }: Props) { const [documents, setDocuments] = useState(projectProps.documents) const [groups, setGroups] = useState(projectProps.groups) + const [contextGroups, setContextGroups] = useState(projectProps.contextGroups) const [selectedAreaId, setSelectedAreaId] = useState('') const [selectedDocumentId, setSelectedDocumentId] = useState('') const [currentSession, setCurrentSession] = useState(new entities.Session()) const updateDocuments = async () => { const response = await GetDocuments() - const { documents, groups } = response + const { documents, groups, contextGroups } = response setDocuments(documents) setGroups(groups) + setContextGroups(contextGroups) return response } @@ -43,6 +46,7 @@ export function ProjectProvider({ children, projectProps }: Props) { const areaMethods = createAreaProviderMethods({ documents, updateDocuments, selectedDocumentId }) const sessionMethods = createSessionProviderMethods({ updateSession, updateDocuments }) const userMarkDownMethods = createUserMarkdownProviderMethods() + const contextGroupMethods = createContextGroupProviderMethods({ updateDocuments }) useEffect(() => { @@ -60,15 +64,18 @@ export function ProjectProvider({ children, projectProps }: Props) { id: '', documents, groups, + contextGroups, selectedAreaId, setSelectedAreaId, selectedDocumentId, setSelectedDocumentId, currentSession, + updateDocuments, ...areaMethods, ...documentMethods, ...sessionMethods, ...userMarkDownMethods, + ...contextGroupMethods, } return diff --git a/frontend/context/Project/types.ts b/frontend/context/Project/types.ts index 9dca7a2..3e37e7c 100644 --- a/frontend/context/Project/types.ts +++ b/frontend/context/Project/types.ts @@ -4,6 +4,7 @@ export type ProjectProps = { id: string, documents: entities.Document[], groups: entities.Group[], + contextGroups: entities.SerializedLinkedProcessedArea[], } export type AddAreaProps = { @@ -40,7 +41,7 @@ export type ProjectContextType = { getSelectedDocument: () => entities.Document | undefined getAreaById: (areaId: string) => entities.Area | undefined getProcessedAreasByDocumentId: (documentId: string) => Promise - requestAddProcessedArea: (processedArea: entities.ProcessedArea) => Promise + requestAddProcessedArea: (processedArea: entities.ProcessedArea) => Promise requestAddArea: (documentId: string, area: AddAreaProps) => Promise requestUpdateArea: (area: AreaProps) => Promise requestDeleteAreaById: (areaId: string) => Promise @@ -65,4 +66,7 @@ export type ProjectContextType = { requestUpdateProcessedWordById: (wordId: string, newTextValue: string) => Promise getProcessedAreaById: (areaId: string) => Promise requestUpdateProcessedArea: (updatedProcessedArea: entities.ProcessedArea) => Promise + requestConnectProcessedAreas: (headId: string, tailId: string) => Promise + getSerializedContextGroups: () => Promise + updateDocuments: () => Promise } & ProjectProps \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 25fd6ef..b5bdb7c 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -16,4 +16,7 @@ module.exports = { }, }, }, + colors: { + brandPrimary: '#dc8dec', + } } \ No newline at end of file diff --git a/frontend/useCases/processImageArea.ts b/frontend/useCases/processImageArea.ts index b4298e9..6e641c5 100644 --- a/frontend/useCases/processImageArea.ts +++ b/frontend/useCases/processImageArea.ts @@ -1,4 +1,4 @@ -import { createScheduler, createWorker } from 'tesseract.js' +import { createScheduler, createWorker, PSM } from 'tesseract.js' import { GetAreaById, GetDocumentById, GetProcessedAreaById, RequestAddProcessedArea, RequestSaveProcessedTextCollection, RequestUpdateProcessedArea } from '../wailsjs/wailsjs/go/ipc/Channel' import { entities } from '../wailsjs/wailsjs/go/models' import loadImage from './loadImage' @@ -79,11 +79,15 @@ const processImageArea = async (documentId: string, areaId: string) => { const existingProcessedArea = await GetProcessedAreaById(areaId) - let didSuccessfullyProcess = false - if (existingProcessedArea.id !== areaId) didSuccessfullyProcess = await RequestAddProcessedArea(newProcessedArea) - else await RequestUpdateProcessedArea(newProcessedArea) - - saveProcessedText() + let didSuccessfullyProcess: boolean // TODO: fix this: this no longer is truthful, returns true or false if there was not a JS error + try { + if (existingProcessedArea.id !== areaId) await RequestAddProcessedArea(newProcessedArea) + else await RequestUpdateProcessedArea(newProcessedArea) + saveProcessedText() + didSuccessfullyProcess = true + } catch (err) { + didSuccessfullyProcess = false + } return didSuccessfullyProcess } diff --git a/frontend/useCases/processImageRect.ts b/frontend/useCases/processImageRect.ts new file mode 100644 index 0000000..470449c --- /dev/null +++ b/frontend/useCases/processImageRect.ts @@ -0,0 +1,103 @@ +import { PSM, createScheduler, createWorker } from 'tesseract.js' +import { GetDocumentById, RequestAddArea, RequestAddProcessedArea } from '../wailsjs/wailsjs/go/ipc/Channel' +import loadImage from './loadImage' +import { entities } from '../wailsjs/wailsjs/go/models' +import { saveProcessedText } from './saveData' + +type rect = { + startX: number, + endX: number, + startY: number, + endY: number, +} + +const processImageRect = async (documentId: string, rectangle: rect): Promise => { + const foundDocument = await GetDocumentById(documentId) + const { path, defaultLanguage } = foundDocument + if (!path || !defaultLanguage) return [] + + const processLanguage = defaultLanguage.processCode + const imageData = await loadImage(path) + + let workerOptions: Partial = {} + if (foundDocument.defaultLanguage.isBundledCustom) { + workerOptions = { + langPath: '/customLanguages', + gzip: false, + // logger: m => console.log(m) + } + } + + const worker = await createWorker(workerOptions) + await worker.loadLanguage(processLanguage) + await worker.initialize(processLanguage) + await worker.setParameters({ + tessedit_pageseg_mode: PSM.AUTO_OSD, + }) + + const scheduler = createScheduler() + scheduler.addWorker(worker) + + const result = await scheduler.addJob('recognize', imageData, { + rectangle: { + left: rectangle.startX, + top: rectangle.startY, + width: rectangle.endX - rectangle.startX, + height: rectangle.endY - rectangle.startY, + } + }) + + const addAreaRequests = result.data.paragraphs.map(async (p: any) => { + const defaultAreaName = p.lines[0]?.words[0]?.text || '' + const area = await RequestAddArea( + documentId, + new entities.Area({ + name: defaultAreaName, + startX: p.bbox.x0, + endX: p.bbox.x1, + startY: p.bbox.y0, + endY: p.bbox.y1, + }) + ) + + const processedArea = await RequestAddProcessedArea(new entities.ProcessedArea({ + id: area.id, + documentId, + order: area.order, + fullText: p.text, + lines: p.lines.map((l: any) => new entities.ProcessedLine({ + fullText: l.text, + words: l.words.map((w: any) => new entities.ProcessedWord({ + areaId: area.id, + fullText: w.text, + direction: w.direction, + confidence: w.confidence, + boundingBox: new entities.ProcessedBoundingBox({ + x0: w.bbox.x0, + y0: w.bbox.y0, + x1: w.bbox.x1, + y1: w.bbox.y1, + }), + symbols: w.symbols.map((s: any) => new entities.ProcessedSymbol({ + fullText: s.text, + confidence: s.confidence, + boundingBox: new entities.ProcessedBoundingBox({ + x0: s.bbox.x0, + y0: s.bbox.y0, + x1: s.bbox.x1, + y1: s.bbox.y1, + }) + })) + })) + })) + })) + return processedArea + }) + + const addAreaResponses = await Promise.allSettled(addAreaRequests) + const areas = addAreaResponses.filter((val): val is PromiseFulfilledResult => val.status === 'fulfilled').map(val => val.value) + await saveProcessedText() + return areas +} + +export default processImageRect diff --git a/frontend/useCases/saveData.ts b/frontend/useCases/saveData.ts index 06f44b1..9a9f45c 100644 --- a/frontend/useCases/saveData.ts +++ b/frontend/useCases/saveData.ts @@ -1,4 +1,7 @@ -import { RequestSaveDocumentCollection, RequestSaveGroupCollection, RequestSaveLocalUserProcessedMarkdownCollection, RequestSaveProcessedTextCollection } from '../wailsjs/wailsjs/go/ipc/Channel' +import { RequestSaveDocumentCollection, RequestSaveGroupCollection, + RequestSaveLocalUserProcessedMarkdownCollection, + RequestSaveProcessedTextCollection, RequestSaveContextGroupCollection +} from '../wailsjs/wailsjs/go/ipc/Channel' const saveDocuments = async () => { try { @@ -36,9 +39,19 @@ const saveUserProcessedMarkdown = async () => { } } +const saveContextGroups = async () => { + try { + const sucessfulSave = await RequestSaveContextGroupCollection() + if (!sucessfulSave) console.error('Could not save ContextGroupCollection') + } catch (err) { + console.error('Could not save ContextGroupCollection: ', err) + } +} + export { saveDocuments, saveGroups, saveProcessedText, saveUserProcessedMarkdown, + saveContextGroups, } \ No newline at end of file diff --git a/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts b/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts index 86df433..d9a7302 100755 --- a/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts +++ b/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts @@ -23,6 +23,8 @@ export function GetProcessedAreasByDocumentId(arg1:string):Promise; +export function GetSerializedContextGroups():Promise>; + export function GetSupportedLanguages():Promise>; export function GetUserMarkdownByDocumentId(arg1:string):Promise; @@ -33,7 +35,7 @@ export function RequestAddDocument(arg1:string,arg2:string):Promise; -export function RequestAddProcessedArea(arg1:entities.ProcessedArea):Promise; +export function RequestAddProcessedArea(arg1:entities.ProcessedArea):Promise; export function RequestChangeAreaOrder(arg1:string,arg2:number):Promise; @@ -43,7 +45,7 @@ export function RequestChangeSessionProjectByName(arg1:string):Promise; export function RequestChooseUserAvatar():Promise; -export function RequestConnectAreaAsTailToNode(arg1:string,arg2:string):Promise; +export function RequestConnectProcessedAreas(arg1:string,arg2:string):Promise; export function RequestDeleteAreaById(arg1:string):Promise; @@ -51,6 +53,10 @@ export function RequestDeleteDocumentAndChildren(arg1:string):Promise; export function RequestDeleteProcessedAreaById(arg1:string):Promise; +export function RequestDisconnectProcessedAreas(arg1:string,arg2:string):Promise; + +export function RequestSaveContextGroupCollection():Promise; + export function RequestSaveDocumentCollection():Promise; export function RequestSaveGroupCollection():Promise; diff --git a/frontend/wailsjs/wailsjs/go/ipc/Channel.js b/frontend/wailsjs/wailsjs/go/ipc/Channel.js index 0cff44e..68a2480 100755 --- a/frontend/wailsjs/wailsjs/go/ipc/Channel.js +++ b/frontend/wailsjs/wailsjs/go/ipc/Channel.js @@ -42,6 +42,10 @@ export function GetProjectByName(arg1) { return window['go']['ipc']['Channel']['GetProjectByName'](arg1); } +export function GetSerializedContextGroups() { + return window['go']['ipc']['Channel']['GetSerializedContextGroups'](); +} + export function GetSupportedLanguages() { return window['go']['ipc']['Channel']['GetSupportedLanguages'](); } @@ -82,8 +86,8 @@ export function RequestChooseUserAvatar() { return window['go']['ipc']['Channel']['RequestChooseUserAvatar'](); } -export function RequestConnectAreaAsTailToNode(arg1, arg2) { - return window['go']['ipc']['Channel']['RequestConnectAreaAsTailToNode'](arg1, arg2); +export function RequestConnectProcessedAreas(arg1, arg2) { + return window['go']['ipc']['Channel']['RequestConnectProcessedAreas'](arg1, arg2); } export function RequestDeleteAreaById(arg1) { @@ -98,6 +102,14 @@ export function RequestDeleteProcessedAreaById(arg1) { return window['go']['ipc']['Channel']['RequestDeleteProcessedAreaById'](arg1); } +export function RequestDisconnectProcessedAreas(arg1, arg2) { + return window['go']['ipc']['Channel']['RequestDisconnectProcessedAreas'](arg1, arg2); +} + +export function RequestSaveContextGroupCollection() { + return window['go']['ipc']['Channel']['RequestSaveContextGroupCollection'](); +} + export function RequestSaveDocumentCollection() { return window['go']['ipc']['Channel']['RequestSaveDocumentCollection'](); } diff --git a/frontend/wailsjs/wailsjs/go/models.ts b/frontend/wailsjs/wailsjs/go/models.ts index a70c55d..f01900b 100755 --- a/frontend/wailsjs/wailsjs/go/models.ts +++ b/frontend/wailsjs/wailsjs/go/models.ts @@ -422,6 +422,22 @@ export namespace entities { } } + export class SerializedLinkedProcessedArea { + areaId: string; + previousId: string; + nextId: string; + + static createFrom(source: any = {}) { + return new SerializedLinkedProcessedArea(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.areaId = source["areaId"]; + this.previousId = source["previousId"]; + this.nextId = source["nextId"]; + } + } export class Session { project: Project; organization: Organization; @@ -481,6 +497,7 @@ export namespace ipc { export class GetDocumentsResponse { documents: entities.Document[]; groups: entities.Group[]; + contextGroups: entities.SerializedLinkedProcessedArea[]; static createFrom(source: any = {}) { return new GetDocumentsResponse(source); @@ -490,6 +507,7 @@ export namespace ipc { if ('string' === typeof source) source = JSON.parse(source); this.documents = this.convertValues(source["documents"], entities.Document); this.groups = this.convertValues(source["groups"], entities.Group); + this.contextGroups = this.convertValues(source["contextGroups"], entities.SerializedLinkedProcessedArea); } convertValues(a: any, classs: any, asMap: boolean = false): any { diff --git a/ipc/ContextGroup.go b/ipc/ContextGroup.go index 0af4db2..620ddaa 100644 --- a/ipc/ContextGroup.go +++ b/ipc/ContextGroup.go @@ -1,22 +1,68 @@ package ipc import ( + contextGroup "textualize/core/ContextGroup" document "textualize/core/Document" "textualize/entities" + "textualize/storage" ) -func (c *Channel) RequestConnectAreaAsTailToNode(tailId string, headId string) bool { - processedAreaOfTail := document.GetProcessedAreaCollection().GetAreaById(tailId) - if processedAreaOfTail == nil { - return false +func (c *Channel) RequestDisconnectProcessedAreas(ancestorAreaId string, descendantAreaId string) bool { + contextGroupCollection := contextGroup.GetContextGroupCollection() + + wasSuccessfulDisconnect := contextGroupCollection.DisconnectProcessedAreas(ancestorAreaId, descendantAreaId) + if wasSuccessfulDisconnect { + wasSuccessfulWrite := c.RequestSaveContextGroupCollection() + return wasSuccessfulWrite } - - processedAreaOfHead := document.GetProcessedAreaCollection().GetAreaById(headId) - if processedAreaOfHead == nil { - return false - } - - entities.GetContextGroupCollection().ConnectAreaAsTailToNode(*processedAreaOfTail, *processedAreaOfHead) - - return true + return false +} + +/* +If a connection already exists, then this method will default to disconnecting the two areas. +*/ +func (c *Channel) RequestConnectProcessedAreas(ancestorAreaId string, descendantAreaId string) bool { + contextGroupCollection := contextGroup.GetContextGroupCollection() + + doesContextGroupAlreadyExist := contextGroupCollection.DoesGroupExistBetweenProcessedAreas(ancestorAreaId, descendantAreaId) + if doesContextGroupAlreadyExist { + return c.RequestDisconnectProcessedAreas(ancestorAreaId, descendantAreaId) + } + + processedAreaCollection := document.GetProcessedAreaCollection() + + ancestorArea := processedAreaCollection.GetAreaById(ancestorAreaId) + descendantArea := processedAreaCollection.GetAreaById(descendantAreaId) + + wasSuccessfulConnect := contextGroupCollection.ConnectProcessedAreas(*ancestorArea, *descendantArea) + if wasSuccessfulConnect { + wasSuccessfulWrite := c.RequestSaveContextGroupCollection() + return wasSuccessfulWrite + } + + return false +} + +func (c *Channel) GetSerializedContextGroups() []entities.SerializedLinkedProcessedArea { + contextGroupCollection := contextGroup.GetContextGroupCollection() + + serializedContextGroups := make([]entities.SerializedLinkedProcessedArea, 0) + for _, group := range contextGroupCollection.Groups { + serializedContextGroups = append(serializedContextGroups, group.Serialize()...) + } + + return serializedContextGroups +} + +func (c *Channel) RequestSaveContextGroupCollection() bool { + contextGroupCollection := contextGroup.GetContextGroupCollection() + projectName := c.GetCurrentSession().Project.Name + + serializedContextGroups := make([]entities.SerializedLinkedProcessedArea, 0) + for _, group := range contextGroupCollection.Groups { + serializedContextGroups = append(serializedContextGroups, group.Serialize()...) + } + + successfulWrite := storage.GetDriver().WriteContextGroupCollection(serializedContextGroups, projectName) + return successfulWrite } diff --git a/ipc/Documents.go b/ipc/Documents.go index 7087bb5..57d56c5 100644 --- a/ipc/Documents.go +++ b/ipc/Documents.go @@ -14,8 +14,9 @@ import ( ) type GetDocumentsResponse struct { - Documents []entities.Document `json:"documents"` - Groups []entities.Group `json:"groups"` + Documents []entities.Document `json:"documents"` + Groups []entities.Group `json:"groups"` + ContextGroups []entities.SerializedLinkedProcessedArea `json:"contextGroups"` } func (c *Channel) GetDocumentById(id string) entities.Document { @@ -26,10 +27,12 @@ func (c *Channel) GetDocumentById(id string) entities.Document { func (c *Channel) GetDocuments() GetDocumentsResponse { documents := document.GetDocumentCollection().Documents groups := document.GetGroupCollection().Groups + contextGroups := c.GetSerializedContextGroups() response := GetDocumentsResponse{ - Groups: make([]entities.Group, 0), - Documents: make([]entities.Document, 0), + Groups: make([]entities.Group, 0), + Documents: make([]entities.Document, 0), + ContextGroups: contextGroups, } for _, d := range documents { diff --git a/ipc/ProcessedDocument.go b/ipc/ProcessedDocument.go index 93571d3..abc0db8 100644 --- a/ipc/ProcessedDocument.go +++ b/ipc/ProcessedDocument.go @@ -1,7 +1,6 @@ package ipc import ( - "fmt" "sort" document "textualize/core/Document" "textualize/entities" @@ -35,7 +34,7 @@ func (c *Channel) GetProcessedAreasByDocumentId(id string) []entities.ProcessedA return sortedAreas } -func (c *Channel) RequestAddProcessedArea(processedArea entities.ProcessedArea) bool { +func (c *Channel) RequestAddProcessedArea(processedArea entities.ProcessedArea) entities.ProcessedArea { for lineIndex, line := range processedArea.Lines { for wordIndex, word := range line.Words { @@ -46,7 +45,7 @@ func (c *Channel) RequestAddProcessedArea(processedArea entities.ProcessedArea) } document.GetProcessedAreaCollection().AddProcessedArea(processedArea) - return true + return *document.GetProcessedAreaCollection().GetAreaById(processedArea.Id) } func (c *Channel) RequestDeleteProcessedAreaById(id string) bool { @@ -75,9 +74,6 @@ func (c *Channel) RequestDeleteProcessedAreaById(id string) bool { } func (c *Channel) RequestUpdateProcessedArea(updatedProcessedArea entities.ProcessedArea) bool { - fmt.Println("updatedProcessedArea") - fmt.Println(&updatedProcessedArea) - fmt.Println() if updatedProcessedArea.Id == "" { return false } @@ -87,14 +83,13 @@ func (c *Channel) RequestUpdateProcessedArea(updatedProcessedArea entities.Proce return false } - successfulAdd := c.RequestAddProcessedArea(updatedProcessedArea) - if !successfulAdd { - return false - } + addedProcessedArea := c.RequestAddProcessedArea(updatedProcessedArea) + return addedProcessedArea.Id != "" - fmt.Println("document.GetProcessedAreaCollection().GetAreaById(updatedProcessedArea.Id)") - fmt.Println(document.GetProcessedAreaCollection().GetAreaById(updatedProcessedArea.Id)) - return true + // if addedProcessedArea.Id != "" { + // return false + // } + // return true } func (c *Channel) RequestUpdateProcessedWordById(wordId string, newTextValue string) bool { diff --git a/ipc/Session.go b/ipc/Session.go index 2b06d75..eb5a344 100644 --- a/ipc/Session.go +++ b/ipc/Session.go @@ -3,6 +3,7 @@ package ipc import ( app "textualize/core/App" consts "textualize/core/Consts" + contextGroup "textualize/core/ContextGroup" document "textualize/core/Document" session "textualize/core/Session" "textualize/entities" @@ -144,6 +145,7 @@ func (c *Channel) RequestChangeSessionProjectByName(projectName string) bool { session.GetInstance().Project = foundProject + // Documents localDocumentCollection := storageDriver.ReadDocumentCollection(projectName) documentCount := len(localDocumentCollection.Documents) readableDocuments := make([]document.Entity, documentCount) @@ -155,6 +157,7 @@ func (c *Channel) RequestChangeSessionProjectByName(projectName string) bool { ProjectId: foundProject.Id, }) + // Groups localGroupsCollection := storageDriver.ReadGroupCollection(projectName) groupCount := len(localGroupsCollection.Groups) readableGroups := make([]entities.Group, groupCount) @@ -167,6 +170,10 @@ func (c *Channel) RequestChangeSessionProjectByName(projectName string) bool { Groups: readableGroups, }) + // Context Groups + localSerializedContextGroups := storageDriver.ReadContextGroupCollection(projectName) + contextGroup.SetContextGroupCollectionBySerialized(localSerializedContextGroups) + // Processed Texts localProcessedAreaCollection := storageDriver.ReadProcessedTextCollection(projectName) areaCount := len(localProcessedAreaCollection.Areas) diff --git a/storage/Local/ContextGroupDriver.go b/storage/Local/ContextGroupDriver.go new file mode 100644 index 0000000..7f14fb2 --- /dev/null +++ b/storage/Local/ContextGroupDriver.go @@ -0,0 +1,22 @@ +package storage + +import ( + "encoding/json" + "textualize/entities" +) + +func (d LocalDriver) WriteContextGroupCollection(serializedContextGroups []entities.SerializedLinkedProcessedArea, projectName string) bool { + jsonData, _ := json.MarshalIndent(serializedContextGroups, "", " ") + writeError := WriteDataToAppDir(jsonData, "/projects/"+projectName+"/", "ContextGroups.json") + return writeError == nil +} + +func (d LocalDriver) ReadContextGroupCollection(projectName string) []entities.SerializedLinkedProcessedArea { + contextGroupCollectionData := make([]entities.SerializedLinkedProcessedArea, 0) + readError := AssignFileDataToStruct("/projects/"+projectName+"/ContextGroups.json", &contextGroupCollectionData) + if readError != nil { + return make([]entities.SerializedLinkedProcessedArea, 0) + } + + return contextGroupCollectionData +} diff --git a/storage/Storage.go b/storage/Storage.go index d31a7a4..bc96a82 100644 --- a/storage/Storage.go +++ b/storage/Storage.go @@ -19,6 +19,8 @@ type Driver interface { ReadProcessedTextCollection(string) entities.ProcessedTextCollection WriteProcessedUserMarkdownCollection(entities.ProcessedUserMarkdownCollection, string) bool ReadProcessedUserMarkdownCollection(string) entities.ProcessedUserMarkdownCollection + WriteContextGroupCollection([]entities.SerializedLinkedProcessedArea, string) bool + ReadContextGroupCollection(string) []entities.SerializedLinkedProcessedArea } var driverInstance Driver