fr.arakne.utils.maps.path.Decoder   A
last analyzed

Complexity

Total Complexity 25

Size/Duplication

Total Lines 188
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 57
c 1
b 0
f 0
dl 0
loc 188
ccs 48
cts 48
cp 1
rs 10
wmc 25

9 Methods

Rating   Name   Duplication   Size   Complexity  
A decode(String) 0 2 1
A Decoder(DofusMap) 0 2 1
A encode(Path) 0 2 1
A encodeWithStartCell(Path) 0 2 1
A pathfinder() 0 2 1
B encode(Path,boolean) 0 27 6
A nextCellByDirection(C,Direction) 0 8 3
A expandRectilinearMove(Path,C,C,Direction) 0 9 3
B decode(String,C) 0 36 8
1
/*
2
 * This file is part of ArakneUtils.
3
 *
4
 * ArakneUtils is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU Lesser General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * ArakneUtils is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU Lesser General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU Lesser General Public License
15
 * along with ArakneUtils.  If not, see <https://www.gnu.org/licenses/>.
16
 *
17
 * Copyright (c) 2017-2020 Vincent Quatrevieux
18
 */
19
20
package fr.arakne.utils.maps.path;
21
22
import fr.arakne.utils.encoding.Base64;
23
import fr.arakne.utils.maps.DofusMap;
24
import fr.arakne.utils.maps.MapCell;
25
import fr.arakne.utils.maps.constant.Direction;
26
import org.checkerframework.checker.nullness.qual.NonNull;
27
import org.checkerframework.checker.nullness.qual.Nullable;
28
import org.checkerframework.dataflow.qual.SideEffectFree;
29
30
import java.util.Optional;
31
32
/**
33
 * Decode map data like paths or directions
34
 */
35
public final class Decoder<C extends @NonNull MapCell> {
36
    private final DofusMap<C> map;
37
38
    /**
39
     * @param map The map to handle
40
     */
41 1
    public Decoder(DofusMap<C> map) {
42 1
        this.map = map;
43 1
    }
44
45
    /**
46
     * Get the immediately next cell if we move by the given direction
47
     * If the next cell is out of the map, an empty optional is returned
48
     *
49
     * @param start The start cell
50
     * @param direction The direction to follow
51
     *
52
     * @return The next cell, wrapped into an optional. If the next cell is outside map, return an empty optional
53
     */
54
    public Optional<C> nextCellByDirection(C start, Direction direction) {
55 1
        final int nextId = start.id() + direction.nextCellIncrement(map.dimensions().width());
56
57 1
        if (nextId >= map.size() || nextId < 0) {
58 1
            return Optional.empty();
59
        }
60
61 1
        return Optional.of(map.get(nextId));
62
    }
63
64
    /**
65
     * Decode compressed path
66
     *
67
     * @param encoded The encoded path
68
     * @param start The start cell. Can be null.
69
     *              Set a value is the encoded path do not contains the start cell (ex: use {@link Decoder#encode(Path, boolean)} with includeStartCell)
70
     *
71
     * @return The path, with list of cells
72
     *
73
     * @throws PathException When an invalid path is given
74
     */
75
    public Path<C> decode(String encoded, @Nullable C start) throws PathException {
76 1
        if (encoded.length() % 3 != 0) {
77 1
            throw new PathException("Invalid path : bad length");
78
        }
79
80 1
        final Path<C> path = new Path<>(this);
81
82
        // Add the start cell on the path
83 1
        if (start != null) {
84 1
            path.add(new PathStep<>(start, Direction.EAST));
85
        }
86
87 1
        for (int i = 0; i < encoded.length() - 2; i += 3) {
88 1
            final char directionChar = encoded.charAt(i);
89
90 1
            if (directionChar < Direction.FIRST_CHAR || directionChar > Direction.LAST_CHAR) {
91 1
                throw new PathException("Invalid direction");
92
            }
93
94 1
            final Direction direction = Direction.byChar(directionChar);
95 1
            final int cell = ((Base64.ord(encoded.charAt(i + 1)) & 15) << 6) + Base64.ord(encoded.charAt(i + 2));
96
97 1
            if (cell >= map.size()) {
98 1
                throw new PathException("Invalid cell number");
99
            }
100
101
            // First cell of the path, without a start cell : add it to the path
102 1
            if (path.isEmpty()) {
103 1
                path.add(new PathStep<>(map.get(cell), Direction.EAST));
104 1
                continue;
105
            }
106
107 1
            expandRectilinearMove(path, path.target(), map.get(cell), direction);
108
        }
109
110 1
        return path;
111
    }
112
113
    /**
114
     * Decode compressed path without a start cell
115
     *
116
     * @param encoded The encoded path. Must contains the start cell
117
118
     * @return The path, with list of cells
119
     *
120
     * @throws PathException When an invalid path is given
121
     */
122
    public Path<C> decode(String encoded) throws PathException {
123 1
        return decode(encoded, null);
124
    }
125
126
    /**
127
     * Encode the computed path, including the start cell
128
     * To decode this path, the start cell should not be provided on the decode method
129
     *
130
     * This method should be used by server to send a path to the client
131
     *
132
     * <code>
133
     *     String encoded = encoder.encodeWithStartCell(myPath);
134
     *     Path decoded = encoder.decode(encoded);
135
     * </code>
136
     *
137
     * @param path The path to encode
138
     *
139
     * @return The encoded path
140
     */
141
    public String encodeWithStartCell(Path<C> path) {
142 1
        return encode(path, true);
143
    }
144
145
    /**
146
     * Encode the computed path, without the start cell
147
     * To decode this path, the start cell must be provided on the decode method
148
     *
149
     * This method should be used by the client to send a path to the server
150
     *
151
     * <code>
152
     *     String encoded = encoder.encode(myPath);
153
     *     Path decoded = encoder.decode(encoded, startCell);
154
     * </code>
155
     *
156
     * @param path The path to encode
157
     *
158
     * @return The encoded path
159
     *
160
     * @see Pathfinder#addFirstCell(boolean) Set to false to generate a path without the start cell
161
     */
162
    public String encode(Path<C> path) {
163 1
        return encode(path, false);
164
    }
165
166
    /**
167
     * Get the pathfinder related to this decoder
168
     *
169
     * @return The pathfinder instance
170
     */
171
    public Pathfinder<C> pathfinder() {
172 1
        return new Pathfinder<>(this);
173
    }
174
175
    private void expandRectilinearMove(Path<C> path, C start, C target, Direction direction) throws PathException {
176 1
        int stepsLimit =  2 * map.dimensions().width() + 1;
177
178 1
        while (!start.equals(target)) {
179 1
            start = nextCellByDirection(start, direction).orElseThrow(() -> new PathException("Invalid cell number"));
180 1
            path.add(new PathStep<>(start, direction));
181
182 1
            if (--stepsLimit < 0) {
183 1
                throw new PathException("Invalid path : bad direction");
184
            }
185
        }
186 1
    }
187
188
    /**
189
     * Encode the computed path
190
     *
191
     * @param path The path to encode
192
     * @param includeStartCell Does the encoded path should contain the start cell or not ?
193
     *
194
     * @return The encoded path
195
     */
196
    @SideEffectFree
197
    private String encode(Path<C> path, boolean includeStartCell) {
198 1
        final StringBuilder encoded = new StringBuilder(path.size() * 3);
199
200
        // The start cell must be added to path without compression
201 1
        if (includeStartCell) {
202 1
            encoded
203 1
                .append(Direction.EAST.toChar())
204 1
                .append(Base64.encode(path.get(0).cell().id(), 2))
205
            ;
206
        }
207
208
        // Start the path at step 1 : the start cell is added before
209 1
        for (int i = (includeStartCell ? 1 : 0); i < path.size(); ++i) {
210 1
            final PathStep<C> step = path.get(i);
211
212 1
            encoded.append(step.direction().toChar());
213
214
            // Skip cells on same direction
215 1
            while (i + 1 < path.size() && path.get(i + 1).direction() == step.direction()) {
216 1
                ++i;
217
            }
218
219 1
            encoded.append(Base64.encode(path.get(i).cell().id(), 2));
220
        }
221
222 1
        return encoded.toString();
223
    }
224
}
225