001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.core.plot.expiration;
020
021import com.plotsquared.core.PlotSquared;
022import com.plotsquared.core.configuration.Settings;
023import com.plotsquared.core.generator.HybridUtils;
024import com.plotsquared.core.plot.Plot;
025import com.plotsquared.core.plot.flag.implementations.AnalysisFlag;
026import com.plotsquared.core.util.MathMan;
027import com.plotsquared.core.util.query.PlotQuery;
028import com.plotsquared.core.util.task.RunnableVal;
029import com.plotsquared.core.util.task.TaskManager;
030import org.apache.logging.log4j.LogManager;
031import org.apache.logging.log4j.Logger;
032
033import java.lang.reflect.Array;
034import java.util.ArrayDeque;
035import java.util.ArrayList;
036import java.util.Arrays;
037import java.util.Iterator;
038import java.util.List;
039import java.util.concurrent.atomic.AtomicInteger;
040
041public class PlotAnalysis {
042
043    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + PlotAnalysis.class.getSimpleName());
044
045    public static boolean running = false;
046    public int changes;
047    public int faces;
048    public int data;
049    public int air;
050    public int variety;
051    public int changes_sd;
052    public int faces_sd;
053    public int data_sd;
054    public int air_sd;
055    public int variety_sd;
056    private int complexity;
057
058    public static PlotAnalysis getAnalysis(Plot plot, Settings.Auto_Clear settings) {
059        final List<Integer> values = plot.getFlag(AnalysisFlag.class);
060        if (!values.isEmpty()) {
061            PlotAnalysis analysis = new PlotAnalysis();
062            analysis.changes = values.get(0); // 2126
063            analysis.faces = values.get(1); // 90
064            analysis.data = values.get(2); // 0
065            analysis.air = values.get(3); // 19100
066            analysis.variety = values.get(4); // 266
067
068            analysis.changes_sd = values.get(5); // 2104
069            analysis.faces_sd = values.get(6); // 89
070            analysis.data_sd = values.get(7); // 0
071            analysis.air_sd = values.get(8); // 18909
072            analysis.variety_sd = values.get(9); // 263
073
074            analysis.complexity = settings != null ? analysis.getComplexity(settings) : 0;
075            return analysis;
076        }
077        return null;
078    }
079
080    public static void analyzePlot(Plot plot, RunnableVal<PlotAnalysis> whenDone) {
081        PlotSquared.platform().injector().getInstance(HybridUtils.class).analyzePlot(plot, whenDone);
082    }
083
084    /**
085     * This will set the optimal modifiers for the plot analysis based on the current plot ratings<br>
086     * - Will be used to calibrate the threshold for plot clearing
087     *
088     * @param whenDone  task to run when done
089     * @param threshold threshold
090     */
091    public static void calcOptimalModifiers(final Runnable whenDone, final double threshold) {
092        if (running) {
093            LOGGER.info("Calibration task already in progress!");
094            return;
095        }
096        if (threshold <= 0 || threshold >= 1) {
097            LOGGER.info("Invalid threshold provided! (Cannot be 0 or 100 as then there's no point in calibrating)");
098            return;
099        }
100        running = true;
101        final List<Plot> plots = PlotQuery.newQuery().allPlots().asList();
102        TaskManager.runTaskAsync(new Runnable() {
103            @Override
104            public void run() {
105                Iterator<Plot> iterator = plots.iterator();
106                LOGGER.info("- Reducing {} plots to those with sufficient data", plots.size());
107                while (iterator.hasNext()) {
108                    Plot plot = iterator.next();
109                    if (plot.getSettings().getRatings() == null || plot.getSettings().getRatings()
110                            .isEmpty()) {
111                        iterator.remove();
112                    } else {
113                        plot.addRunning();
114                    }
115                }
116
117                if (plots.size() < 3) {
118                    LOGGER.info("Calibration cancelled due to insufficient comparison data, please try again later");
119                    running = false;
120                    for (Plot plot : plots) {
121                        plot.removeRunning();
122                    }
123                    return;
124                }
125                LOGGER.info("- Analyzing plot contents (this may take a while)");
126
127                int[] changes = new int[plots.size()];
128                int[] faces = new int[plots.size()];
129                int[] data = new int[plots.size()];
130                int[] air = new int[plots.size()];
131                int[] variety = new int[plots.size()];
132
133                int[] changes_sd = new int[plots.size()];
134                int[] faces_sd = new int[plots.size()];
135                int[] data_sd = new int[plots.size()];
136                int[] air_sd = new int[plots.size()];
137                int[] variety_sd = new int[plots.size()];
138
139                final int[] ratings = new int[plots.size()];
140
141                final AtomicInteger mi = new AtomicInteger(0);
142
143                Thread ratingAnalysis = new Thread(() -> {
144                    for (; mi.intValue() < plots.size(); mi.incrementAndGet()) {
145                        int i = mi.intValue();
146                        Plot plot = plots.get(i);
147                        ratings[i] = (int) (
148                                (plot.getAverageRating() + plot.getSettings().getRatings().size())
149                                        * 100);
150                        LOGGER.info(" | {} (rating) {}", plot, ratings[i]);
151
152                    }
153                });
154                ratingAnalysis.start();
155
156                ArrayDeque<Plot> plotsQueue = new ArrayDeque<>(plots);
157                while (true) {
158                    final Plot queuePlot = plotsQueue.poll();
159                    if (queuePlot == null) {
160                        break;
161                    }
162                    LOGGER.info(" | {}", queuePlot);
163
164                    final Object lock = new Object();
165                    TaskManager.runTask(new Runnable() {
166                        @Override
167                        public void run() {
168                            analyzePlot(queuePlot, new RunnableVal<>() {
169                                @Override
170                                public void run(PlotAnalysis value) {
171                                    try {
172                                        synchronized (this) {
173                                            wait(10000);
174                                        }
175                                    } catch (InterruptedException e) {
176                                        e.printStackTrace();
177                                    }
178                                    synchronized (lock) {
179                                        queuePlot.removeRunning();
180                                        lock.notify();
181                                    }
182                                }
183                            });
184                        }
185                    });
186                    try {
187                        synchronized (lock) {
188                            lock.wait();
189                        }
190                    } catch (InterruptedException e) {
191                        e.printStackTrace();
192                    }
193                }
194
195                LOGGER.info("- Waiting on plot rating thread: {}%", mi.intValue() * 100 / plots.size());
196
197                try {
198                    ratingAnalysis.join();
199                } catch (InterruptedException e) {
200                    e.printStackTrace();
201                }
202
203                LOGGER.info("- Processing and grouping single plot analysis for bulk processing");
204
205                for (int i = 0; i < plots.size(); i++) {
206                    Plot plot = plots.get(i);
207                    LOGGER.info("| {}", plot);
208
209                    PlotAnalysis analysis = plot.getComplexity(null);
210
211                    changes[i] = analysis.changes;
212                    faces[i] = analysis.faces;
213                    data[i] = analysis.data;
214                    air[i] = analysis.air;
215                    variety[i] = analysis.variety;
216
217                    changes_sd[i] = analysis.changes_sd;
218                    faces_sd[i] = analysis.faces_sd;
219                    data_sd[i] = analysis.data_sd;
220                    air_sd[i] = analysis.air_sd;
221                    variety_sd[i] = analysis.variety_sd;
222                }
223
224                LOGGER.info("- Calculating rankings");
225
226                int[] rankRatings = rank(ratings);
227                int n = rankRatings.length;
228
229                int optimalIndex = (int) Math.round((1 - threshold) * (n - 1));
230
231                LOGGER.info("- Calculating rank correlation: ");
232                LOGGER.info(
233                        "- The analyzed plots which were processed and put into bulk data will be compared and correlated to the plot ranking");
234                LOGGER.info(
235                        "- The calculated correlation constant will then be used to calibrate the threshold for auto plot clearing");
236
237                Settings.Auto_Clear settings = new Settings.Auto_Clear();
238
239                int[] rankChanges = rank(changes);
240                int[] sdChanges = getSD(rankChanges, rankRatings);
241                int[] varianceChanges = square(sdChanges);
242                int sumChanges = sum(varianceChanges);
243                double factorChanges = getCC(n, sumChanges);
244                settings.CALIBRATION.CHANGES = factorChanges == 1 ?
245                        0 :
246                        (int) (factorChanges * 1000 / MathMan.getMean(changes));
247
248                LOGGER.info("- | changes {}", factorChanges);
249
250                int[] rankFaces = rank(faces);
251                int[] sdFaces = getSD(rankFaces, rankRatings);
252                int[] varianceFaces = square(sdFaces);
253                int sumFaces = sum(varianceFaces);
254                double factorFaces = getCC(n, sumFaces);
255                settings.CALIBRATION.FACES =
256                        factorFaces == 1 ? 0 : (int) (factorFaces * 1000 / MathMan.getMean(faces));
257
258                LOGGER.info("- | faces {}", factorFaces);
259
260                int[] rankData = rank(data);
261                int[] sdData = getSD(rankData, rankRatings);
262                int[] variance_data = square(sdData);
263                int sum_data = sum(variance_data);
264                double factor_data = getCC(n, sum_data);
265                settings.CALIBRATION.DATA =
266                        factor_data == 1 ? 0 : (int) (factor_data * 1000 / MathMan.getMean(data));
267
268                LOGGER.info("- | data {}", factor_data);
269
270                int[] rank_air = rank(air);
271                int[] sd_air = getSD(rank_air, rankRatings);
272                int[] variance_air = square(sd_air);
273                int sum_air = sum(variance_air);
274                double factor_air = getCC(n, sum_air);
275                settings.CALIBRATION.AIR =
276                        factor_air == 1 ? 0 : (int) (factor_air * 1000 / MathMan.getMean(air));
277
278                LOGGER.info("- | air {}", factor_air);
279
280
281                int[] rank_variety = rank(variety);
282                int[] sd_variety = getSD(rank_variety, rankRatings);
283                int[] variance_variety = square(sd_variety);
284                int sum_variety = sum(variance_variety);
285                double factor_variety = getCC(n, sum_variety);
286                settings.CALIBRATION.VARIETY = factor_variety == 1 ?
287                        0 :
288                        (int) (factor_variety * 1000 / MathMan.getMean(variety));
289
290                LOGGER.info("- | variety {}", factor_variety);
291
292                int[] rank_changes_sd = rank(changes_sd);
293                int[] sd_changes_sd = getSD(rank_changes_sd, rankRatings);
294                int[] variance_changes_sd = square(sd_changes_sd);
295                int sum_changes_sd = sum(variance_changes_sd);
296                double factor_changes_sd = getCC(n, sum_changes_sd);
297                settings.CALIBRATION.CHANGES_SD = factor_changes_sd == 1 ?
298                        0 :
299                        (int) (factor_changes_sd * 1000 / MathMan.getMean(changes_sd));
300
301                LOGGER.info("- | changed_sd {}", factor_changes_sd);
302
303                int[] rank_faces_sd = rank(faces_sd);
304                int[] sd_faces_sd = getSD(rank_faces_sd, rankRatings);
305                int[] variance_faces_sd = square(sd_faces_sd);
306                int sum_faces_sd = sum(variance_faces_sd);
307                double factor_faces_sd = getCC(n, sum_faces_sd);
308                settings.CALIBRATION.FACES_SD = factor_faces_sd == 1 ?
309                        0 :
310                        (int) (factor_faces_sd * 1000 / MathMan.getMean(faces_sd));
311
312                LOGGER.info("- | faced_sd {}", factor_faces_sd);
313
314                int[] rank_data_sd = rank(data_sd);
315                int[] sd_data_sd = getSD(rank_data_sd, rankRatings);
316                int[] variance_data_sd = square(sd_data_sd);
317                int sum_data_sd = sum(variance_data_sd);
318                double factor_data_sd = getCC(n, sum_data_sd);
319                settings.CALIBRATION.DATA_SD = factor_data_sd == 1 ?
320                        0 :
321                        (int) (factor_data_sd * 1000 / MathMan.getMean(data_sd));
322
323                LOGGER.info("- | data_sd {}", factor_data_sd);
324
325                int[] rank_air_sd = rank(air_sd);
326                int[] sd_air_sd = getSD(rank_air_sd, rankRatings);
327                int[] variance_air_sd = square(sd_air_sd);
328                int sum_air_sd = sum(variance_air_sd);
329                double factor_air_sd = getCC(n, sum_air_sd);
330                settings.CALIBRATION.AIR_SD =
331                        factor_air_sd == 1 ? 0 : (int) (factor_air_sd * 1000 / MathMan.getMean(air_sd));
332
333                LOGGER.info("- | air_sd {}", factor_air_sd);
334
335                int[] rank_variety_sd = rank(variety_sd);
336                int[] sd_variety_sd = getSD(rank_variety_sd, rankRatings);
337                int[] variance_variety_sd = square(sd_variety_sd);
338                int sum_variety_sd = sum(variance_variety_sd);
339                double factor_variety_sd = getCC(n, sum_variety_sd);
340                settings.CALIBRATION.VARIETY_SD = factor_variety_sd == 1 ?
341                        0 :
342                        (int) (factor_variety_sd * 1000 / MathMan.getMean(variety_sd));
343
344                LOGGER.info("- | variety_sd {}", factor_variety_sd);
345
346                int[] complexity = new int[n];
347
348                LOGGER.info("Calculating threshold");
349
350                int max = 0;
351                int min = 0;
352                for (int i = 0; i < n; i++) {
353                    Plot plot = plots.get(i);
354                    PlotAnalysis analysis = plot.getComplexity(settings);
355                    complexity[i] = analysis.complexity;
356                    if (analysis.complexity < min) {
357                        min = analysis.complexity;
358                    } else if (analysis.complexity > max) {
359                        max = analysis.complexity;
360                    }
361                }
362                int optimalComplexity = Integer.MAX_VALUE;
363                if (min > 0 && max < 102400) { // If low size, use my fast ranking algorithm
364                    int[] rankComplexity = rank(complexity, max + 1);
365                    for (int i = 0; i < n; i++) {
366                        if (rankComplexity[i] == optimalIndex) {
367                            optimalComplexity = complexity[i];
368                            break;
369                        }
370                    }
371                    logln("Complexity: ");
372                    logln(rankComplexity);
373                    logln("Ratings: ");
374                    logln(rankRatings);
375                    logln("Correlation: ");
376                    logln(getCC(n, sum(square(getSD(rankComplexity, rankRatings)))));
377                    if (optimalComplexity == Integer.MAX_VALUE) {
378                        LOGGER.info("Insufficient data to determine correlation! {} | {}",
379                                optimalIndex, n
380                        );
381                        running = false;
382                        for (Plot plot : plots) {
383                            plot.removeRunning();
384                        }
385                        return;
386                    }
387                } else { // Use the fast radix sort algorithm
388                    int[] sorted = complexity.clone();
389                    sort(sorted);
390                    logln("Complexity: ");
391                    logln(complexity);
392                    logln("Ratings: ");
393                    logln(rankRatings);
394                }
395
396                // Save calibration
397                LOGGER.info("Saving calibration");
398                Settings.AUTO_CLEAR.put("auto-calibrated", settings);
399                Settings.save(PlotSquared.get().getWorldsFile());
400                running = false;
401                for (Plot plot : plots) {
402                    plot.removeRunning();
403                }
404                LOGGER.info("Done!");
405                whenDone.run();
406            }
407        });
408    }
409
410    public static void logln(Object obj) {
411        LOGGER.info("" + log(obj));
412    }
413
414    public static String log(Object obj) {
415        StringBuilder result = new StringBuilder();
416        if (obj.getClass().isArray()) {
417            String prefix = "";
418
419            for (int i = 0; i < Array.getLength(obj); i++) {
420                result.append(prefix).append(log(Array.get(obj, i)));
421                prefix = ",";
422            }
423            return "( " + result + " )";
424        } else if (obj instanceof List<?>) {
425            String prefix = "";
426            for (Object element : (List<?>) obj) {
427                result.append(prefix).append(log(element));
428                prefix = ",";
429            }
430            return "[ " + result + " ]";
431        } else {
432            return obj.toString();
433        }
434    }
435
436    /**
437     * Get correlation coefficient.
438     *
439     * @param n   n
440     * @param sum sum
441     * @return result
442     */
443    public static double getCC(int n, int sum) {
444        return 1 - 6 * (double) sum / (n * (n * n - 1));
445    }
446
447    /**
448     * Calls {@code Arrays.stream(array).sum()}
449     *
450     * @param array array
451     * @return sum
452     */
453    public static int sum(int[] array) {
454        return Arrays.stream(array).sum();
455    }
456
457    /**
458     * A simple array squaring algorithm.
459     * - Used for calculating the variance
460     *
461     * @param array array
462     * @return result
463     */
464    public static int[] square(int[] array) {
465        array = array.clone();
466        for (int i = 0; i < array.length; i++) {
467            array[i] *= array[i];
468        }
469        return array;
470    }
471
472    /**
473     * An optimized lossy standard deviation algorithm.
474     *
475     * @param ranks ranks
476     * @return result
477     */
478    public static int[] getSD(int[]... ranks) {
479        if (ranks.length == 0) {
480            return null;
481        }
482        int[] result = new int[ranks[0].length];
483        for (int j = 0; j < ranks[0].length; j++) {
484            int sum = 0;
485            for (int[] rank : ranks) {
486                sum += rank[j];
487            }
488            int mean = sum / ranks.length;
489            int sd = 0;
490            for (int[] rank : ranks) {
491                int value = rank[j];
492                sd += value < mean ? mean - value : value - mean;
493            }
494            result[j] = sd;
495        }
496        return result;
497    }
498
499    /**
500     * An optimized algorithm for ranking a very specific set of inputs<br>
501     * - Input is an array of int with a max size of 102400<br>
502     * - A reduced sample space allows for sorting (and ranking in this case) in linear time
503     *
504     * @param input input
505     * @return result
506     */
507    public static int[] rank(int[] input) {
508        return rank(input, 102400);
509    }
510
511    /**
512     * An optimized algorithm for ranking a very specific set of inputs
513     *
514     * @param input input
515     * @param size  size
516     * @return result
517     */
518    public static int[] rank(int[] input, int size) {
519        int[] cache = new int[size];
520        int max = 0;
521        if (input.length < size) {
522            for (int value : input) {
523                if (value > max) {
524                    max = value;
525                }
526                cache[value]++;
527            }
528        } else {
529            max = cache.length - 1;
530            for (int value : input) {
531                cache[value]++;
532            }
533        }
534        int last = 0;
535        for (int i = max; i >= 0; i--) {
536            if (cache[i] != 0) {
537                cache[i] += last;
538                last = cache[i];
539                if (last == input.length) {
540                    break;
541                }
542            }
543        }
544
545        int[] ranks = new int[input.length];
546        for (int i = 0; i < input.length; i++) {
547            int index = input[i];
548            ranks[i] = cache[index];
549            cache[index]--;
550        }
551        return ranks;
552    }
553
554    @SuppressWarnings("unchecked")
555    public static void sort(int[] input) {
556        int SIZE = 10;
557        List<Integer>[] bucket = new ArrayList[SIZE];
558        for (int i = 0; i < bucket.length; i++) {
559            bucket[i] = new ArrayList<>();
560        }
561        boolean maxLength = false;
562        int placement = 1;
563        while (!maxLength) {
564            maxLength = true;
565            for (Integer i : input) {
566                int tmp = i / placement;
567                bucket[tmp % SIZE].add(i);
568                if (maxLength && tmp > 0) {
569                    maxLength = false;
570                }
571            }
572            int a = 0;
573            for (int b = 0; b < SIZE; b++) {
574                for (Integer i : bucket[b]) {
575                    input[a++] = i;
576                }
577                bucket[b].clear();
578            }
579            placement *= SIZE;
580        }
581    }
582
583    public List<Integer> asList() {
584        return Arrays
585                .asList(this.changes, this.faces, this.data, this.air, this.variety, this.changes_sd,
586                        this.faces_sd, this.data_sd, this.air_sd, this.variety_sd
587                );
588    }
589
590    public int getComplexity(Settings.Auto_Clear settings) {
591        Settings.Auto_Clear.CALIBRATION modifiers = settings.CALIBRATION;
592        if (this.complexity != 0) {
593            return this.complexity;
594        }
595        this.complexity = this.changes * modifiers.CHANGES + this.faces * modifiers.FACES
596                + this.data * modifiers.DATA + this.air * modifiers.AIR
597                + this.variety * modifiers.VARIETY + this.changes_sd * modifiers.CHANGES_SD
598                + this.faces_sd * modifiers.FACES_SD + this.data_sd * modifiers.DATA_SD
599                + this.air_sd * modifiers.AIR_SD + this.variety_sd * modifiers.VARIETY_SD;
600        return this.complexity;
601    }
602
603}